From f25d9e06484a0fbc221ec7da5855f93bae906366 Mon Sep 17 00:00:00 2001 From: dtoro Date: Sun, 31 May 2026 01:18:43 +0200 Subject: [PATCH 01/25] hermes-agent: onboard Nous-Hermes-on-Goose to homelab clients MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `bootstrap.sh --with-hermes` installs the Goose CLI, drops a Goose config pinning the OpenRouter provider + Nous Hermes model + the homelab MCP extension, symlinks `bin/hermes` and HERMES.md, and links HERMES.md as `.goosehints` so the persona is injected as the system prompt every session. `bin/hermes` decrypts `secrets/openrouter-api-key.yaml` via the existing `homelab secret` flow and execs `goose session`. `homelab client add --with-hermes` grants the new sops secret to the host's age_pubkey at finalize time (parallel to the existing shared-secrets grant). `client remove` revokes it. `operations/hermes-agent.md` covers the end-to-end flow, verification, troubleshooting, and queues one follow-up: the MCP server still runs SSE-only but Goose 1.x deprecated SSE — the Goose config targets `streamable_http` and the `homelab` extension won't connect until `mcp/server.py` migrates. The `developer` extension (shell + edit + `homelab` CLI) carries the agent in the meantime. Co-Authored-By: Claude Opus 4.7 (1M context) --- .sops.yaml | 10 ++ bin/hermes | 36 ++++++ bin/homelab | 43 +++++-- bootstrap.sh | 115 ++++++++++++++++++ operations/agent-enrollment.md | 7 ++ operations/hermes-agent.md | 199 ++++++++++++++++++++++++++++++++ secrets/openrouter-api-key.yaml | 30 +++++ 7 files changed, 432 insertions(+), 8 deletions(-) create mode 100755 bin/hermes create mode 100644 operations/hermes-agent.md create mode 100644 secrets/openrouter-api-key.yaml diff --git a/.sops.yaml b/.sops.yaml index a04fcc1..3976197 100644 --- a/.sops.yaml +++ b/.sops.yaml @@ -62,4 +62,14 @@ creation_rules: age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6, age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0, age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6 + + - path_regex: ^secrets/openrouter-api-key\.yaml$ + # OpenRouter API key consumed by the `hermes` wrapper (bin/hermes) when + # spawning a Goose session. Recipients are any host that should run a + # Nous-Hermes agent. Add a host's age_pubkey here, then + # `sops updatekeys -y secrets/openrouter-api-key.yaml`. + # See operations/hermes-agent.md. + age: >- + age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6, + age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6 # webhook noop 2026-05-20T18:16:57+02:00 diff --git a/bin/hermes b/bin/hermes new file mode 100755 index 0000000..596029a --- /dev/null +++ b/bin/hermes @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# +# hermes — launch a Goose session pre-wired with the homelab persona, +# OpenRouter (Nous Hermes) provider, and the homelab MCP server. +# +# See operations/hermes-agent.md for the full onboarding flow. + +set -euo pipefail + +die() { echo "hermes: $*" >&2; exit 1; } + +command -v goose >/dev/null \ + || die "goose binary not found — re-run bootstrap.sh with --with-hermes" +command -v homelab >/dev/null \ + || die "homelab CLI not found — is this client bootstrapped?" + +# Decrypt OpenRouter API key. +# `homelab secret` re-execs via sudo for non-root users (age key is 0600 root). +SECRET_YAML=$(homelab secret openrouter-api-key 2>&1) || \ + die "could not decrypt secrets/openrouter-api-key.yaml — this host probably +isn't a recipient yet. See operations/hermes-agent.md ('Granting the OpenRouter +key to a new host'). sops output: +${SECRET_YAML}" + +API_KEY=$(printf '%s' "$SECRET_YAML" | python3 -c \ + 'import sys, yaml; print(yaml.safe_load(sys.stdin)["api_key"])') + +case "$API_KEY" in + PLACEHOLDER_*|"") + die "openrouter-api-key.yaml still contains the placeholder; operator +must run \`sops secrets/openrouter-api-key.yaml\` on hubris to insert a real +\`sk-or-...\` key and push the change." ;; +esac + +export OPENROUTER_API_KEY="$API_KEY" +exec goose session "$@" diff --git a/bin/homelab b/bin/homelab index aa3ef04..ee1621c 100755 --- a/bin/homelab +++ b/bin/homelab @@ -199,6 +199,12 @@ SHARED_SECRETS = [ ("secrets/netbird-authentik-oidc.yaml", "^secrets/netbird-authentik-oidc\\.yaml$"), ] +# Secrets granted only to hosts that opt into running the Hermes agent +# (via `homelab client add --finalize-pubkey ... --with-hermes`). +HERMES_SECRETS = [ + ("secrets/openrouter-api-key.yaml", "^secrets/openrouter-api-key\\.yaml$"), +] + # -------- comment-preserving inventory.yaml edits -------- # yaml.safe_load + safe_dump round-trips strip every comment, which is fine @@ -360,11 +366,15 @@ def _add_recipient_to_sops_policy(sops_path: Path, path_regex_pattern: str, pubk return True -def _grant_shared_secrets(pubkey: str) -> None: - """Add `pubkey` to the recipient list of every shared secret + re-key.""" +def _grant_shared_secrets(pubkey: str, secrets: list[tuple[str, str]] = SHARED_SECRETS) -> None: + """Add `pubkey` to the recipient list of every listed secret + re-key. + + `secrets` defaults to SHARED_SECRETS; pass HERMES_SECRETS to grant the + Hermes-only set. + """ sops_path = CONTEXT / ".sops.yaml" env = {**os.environ, "SOPS_AGE_KEY_FILE": str(AGE_KEY)} - for rel_path, pattern in SHARED_SECRETS: + for rel_path, pattern in secrets: target = CONTEXT / rel_path if not target.exists(): print(f" skipping {rel_path}: file does not exist yet") @@ -440,11 +450,14 @@ def _remove_recipient_from_sops_policy(sops_path: Path, path_regex_pattern: str, return True -def _revoke_shared_secrets(pubkey: str) -> None: - """Remove `pubkey` from every shared-secret rule + re-key the files.""" +def _revoke_shared_secrets(pubkey: str, secrets: list[tuple[str, str]] = SHARED_SECRETS) -> None: + """Remove `pubkey` from every listed secret rule + re-key the files. + + Defaults to SHARED_SECRETS; pass HERMES_SECRETS to revoke the Hermes-only set. + """ sops_path = CONTEXT / ".sops.yaml" env = {**os.environ, "SOPS_AGE_KEY_FILE": str(AGE_KEY)} - for rel_path, pattern in SHARED_SECRETS: + for rel_path, pattern in secrets: target = CONTEXT / rel_path if not target.exists(): continue @@ -1033,7 +1046,8 @@ def cmd_client_add(args: argparse.Namespace) -> int: return subprocess.call(["sudo", "-E", sys.argv[0], "client", "add"] + ([args.name] if args.name else []) + (["--finalize-pubkey", args.finalize_pubkey] - if args.finalize_pubkey else [])) + if args.finalize_pubkey else []) + + (["--with-hermes"] if args.with_hermes else [])) name = args.name inv = inventory() if not args.finalize_pubkey: @@ -1054,6 +1068,7 @@ def cmd_client_add(args: argparse.Namespace) -> int: print(f" 2. On {name}: curl -fsSL /dtoro/Homelab-Docs/raw/main/bootstrap.sh | sudo bash") print(f" 3. bootstrap prints an age pubkey — bring it back here and run:") print(f" homelab client add {name} --finalize-pubkey ") + print(f" (append --with-hermes to also grant the Hermes agent's OpenRouter key.)") return 0 # finalize_pubkey path @@ -1065,8 +1080,13 @@ def cmd_client_add(args: argparse.Namespace) -> int: print(f"set age_pubkey for {name}") print("granting shared secrets...") _grant_shared_secrets(pubkey) + commit_subject = f"client-add: {name} (finalize age_pubkey + grant shared secrets)" + if args.with_hermes: + print("granting hermes-only secrets...") + _grant_shared_secrets(pubkey, HERMES_SECRETS) + commit_subject = f"client-add: {name} (finalize age_pubkey + grant shared + hermes secrets)" push_inventory( - f"client-add: {name} (finalize age_pubkey + grant shared secrets)", + commit_subject, extra_paths=[".sops.yaml", "secrets/"], ) print(f"finalized {name}.") @@ -1103,6 +1123,9 @@ def cmd_client_remove(args: argparse.Namespace) -> int: if pubkey: print("revoking shared secrets...") _revoke_shared_secrets(pubkey) + # Also revoke from hermes-only secrets; idempotent if the pubkey + # was never on those rules (logs a "not present" warning, no harm). + _revoke_shared_secrets(pubkey, HERMES_SECRETS) else: print(f" note: no age_pubkey recorded for {name} — skipping sops re-key") @@ -1518,6 +1541,10 @@ def main() -> int: csub_add.add_argument("name") csub_add.add_argument("--finalize-pubkey", default=None, help="set/update age_pubkey for an already-added client") + csub_add.add_argument("--with-hermes", action="store_true", + help="also grant secrets/openrouter-api-key.yaml so this " + "host can run the Hermes agent (see " + "operations/hermes-agent.md). Combine with --finalize-pubkey.") csub_add.set_defaults(func=cmd_client_add) csub_rm = csub.add_parser("remove") csub_rm.add_argument("name") diff --git a/bootstrap.sh b/bootstrap.sh index ec6b275..239e503 100755 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -5,6 +5,7 @@ # curl -fsSL https://git.hubris.network/dtoro/Homelab-Docs/raw/main/bootstrap.sh \ # | sudo bash # curl ... | sudo bash -s -- --with-mcp # also wire Claude's .mcp.json +# curl ... | sudo bash -s -- --with-hermes # also install Goose + Hermes wrapper # curl ... | sudo bash -s -- --dry-run # show what would happen # curl ... | sudo bash -s -- --no-secrets # skip age-key issuance # @@ -23,8 +24,11 @@ CLONE_DIR="${HOMELAB_CONTEXT_DIR:-/opt/homelab-context}" ISSUANCE_URL_NETBIRD="${HOMELAB_ISSUANCE_NETBIRD:-https://secrets.hubris.network/issue}" ISSUANCE_URL_TAILSCALE="${HOMELAB_ISSUANCE_TAILSCALE:-https://secrets.hubris.network/issue}" MCP_URL="${HOMELAB_MCP_URL:-https://mcp.hubris.network/sse}" +HERMES_MCP_URI="${HOMELAB_HERMES_MCP_URI:-https://mcp.hubris.network/mcp}" +HERMES_MODEL="${HOMELAB_HERMES_MODEL:-nousresearch/hermes-4-405b}" WITH_MCP=0 +WITH_HERMES=0 DRY_RUN=0 NO_SECRETS=0 GITEA_TOKEN="${HOMELAB_GITEA_TOKEN:-}" @@ -34,6 +38,7 @@ GITEA_USER="${HOMELAB_GITEA_USER:-dtoro}" while [ $# -gt 0 ]; do case "$1" in --with-mcp) WITH_MCP=1; shift ;; + --with-hermes) WITH_HERMES=1; shift ;; --dry-run) DRY_RUN=1; shift ;; --no-secrets) NO_SECRETS=1; shift ;; --gitea-token) GITEA_TOKEN="$2"; shift 2 ;; @@ -403,6 +408,116 @@ PYEOF fi fi +# -------- Hermes (Goose + Nous Hermes) wiring -------- +# Installs the Goose CLI binary system-wide, symlinks the `hermes` wrapper +# and HERMES.md persona, and drops a Goose config that pins the OpenRouter +# provider, the Nous Hermes model, and the homelab MCP extension. +# See operations/hermes-agent.md. +if [ "$WITH_HERMES" -eq 1 ]; then + # Resolve the operator's home (SUDO_USER under `sudo bash`). + if [ -n "${SUDO_USER:-}" ] && [ "$SUDO_USER" != "root" ]; then + H_USER="$SUDO_USER" + H_HOME=$(eval echo "~$SUDO_USER") + else + H_USER="root" + H_HOME="$HOME" + fi + + # 1. Install Goose binary at /usr/local/bin/goose (idempotent). + if ! command -v goose >/dev/null 2>&1; then + echo "[bootstrap] installing Goose CLI" + if [ "$DRY_RUN" -eq 1 ]; then + echo "+ would run upstream goose installer and symlink to /usr/local/bin/goose" + else + # Upstream installer drops the binary at ~/.local/bin/goose for the + # invoking user. We run it as $H_USER then symlink system-wide. + sudo -u "$H_USER" \ + env CONFIGURE=false \ + bash -c 'curl -fsSL https://github.com/aaif-goose/goose/releases/download/stable/download_cli.sh | bash' + if [ -x "$H_HOME/.local/bin/goose" ]; then + ln -sfn "$H_HOME/.local/bin/goose" /usr/local/bin/goose + else + echo "[bootstrap] WARNING: goose binary not found at $H_HOME/.local/bin/goose after install" >&2 + fi + fi + else + echo "[bootstrap] goose already installed: $(command -v goose)" + fi + + # 2. Symlink hermes wrapper. + echo "[bootstrap] linking hermes CLI to /usr/local/bin/hermes" + run "ln -sfn '$CLONE_DIR/bin/hermes' /usr/local/bin/hermes" + + # 3. Symlink HERMES.md persona. The hermes wrapper does not need it — the + # Goose config below references the canonical clone path — but operators + # frequently `cat /root/HERMES.md` to inspect the persona, mirroring the + # AGENTS.md convention above. + case "$OS" in + Linux) HERMES_LINK=/root/HERMES.md ;; + Darwin) HERMES_LINK=/etc/HERMES.md ;; + esac + run "ln -sfn '$CLONE_DIR/HERMES.md' '$HERMES_LINK'" + echo "[bootstrap] linked HERMES.md → $HERMES_LINK" + + # 4. Drop the Goose config. Idempotent YAML merge — preserves any keys the + # operator added by hand, overwrites only the keys we manage. + GOOSE_DIR="$H_HOME/.config/goose" + GOOSE_CONFIG="$GOOSE_DIR/config.yaml" + GOOSEHINTS="$GOOSE_DIR/.goosehints" + run "mkdir -p '$GOOSE_DIR'" + + PY_GOOSE_MERGE=$(cat </dev/null || true + fi +fi + # -------- netbird tuning (skip per-session SSO for ssh into mesh peers) -------- # Apply the SSH JWT cache TTL so `ssh ... .netbird.selfhosted` doesn't trigger # device-code SSO on every connection. Flag added in netbird 0.71.x diff --git a/operations/agent-enrollment.md b/operations/agent-enrollment.md index 7140cc8..1003557 100644 --- a/operations/agent-enrollment.md +++ b/operations/agent-enrollment.md @@ -5,6 +5,10 @@ joins the cross-client context system: a `/opt/homelab-context/` clone of this repo that auto-syncs every 5 min, a per-client age key for SOPS decryption, the `homelab` CLI, and an MCP endpoint in Claude Code's config. +> Onboarding a Nous-Hermes-powered Goose agent on top of standard enrollment? +> See [hermes-agent.md](./hermes-agent.md). It uses the same `bootstrap.sh` +> with an additional `--with-hermes` flag. + Architecture in [project_homelab_context_plan](https://… memory link); the operational reference is here. @@ -274,6 +278,9 @@ The CLI prints a follow-up checklist that the operator must do manually: ## Changelog +### 2026-05-31 — cross-link to hermes-agent.md +Added a sibling page covering Nous-Hermes-on-Goose enrollment ([hermes-agent.md](./hermes-agent.md)) and noted it at the top of this page. The Hermes flow extends `bootstrap.sh` with `--with-hermes` and `homelab client add` with the same flag; it does not change the underlying enrollment steps documented here. + ### 2026-05-21 — netbird-ssh JWT issuer + username + LAN-fallback troubleshooting rows Added three rows to the troubleshooting table covering issues surfaced during the netbird vanilla migration: (1) post-migration SSH JWT validator cache stuck on old Dex issuer (full `systemctl stop/start` required, not `restart`), (2) `user not found` from netbird-ssh's local-username default (use explicit `root@`), and (3) homelab CLI's LAN→netbird-FQDN fallback for off-LAN operators. Companion code change: per-host `ssh.user` field in `inventory.yaml` + `homelab` CLI's `ssh_target()` helper. diff --git a/operations/hermes-agent.md b/operations/hermes-agent.md new file mode 100644 index 0000000..e39d56e --- /dev/null +++ b/operations/hermes-agent.md @@ -0,0 +1,199 @@ +# Hermes agent — Nous-Hermes-powered Goose sessions on a homelab client + +Onboards [Nous Research's Hermes](https://nousresearch.com/) (a fine-tuned +Llama variant) as a working terminal agent on a homelab client. Builds on top +of standard client enrollment (see [agent-enrollment.md](./agent-enrollment.md)) +— this page covers only the Hermes-specific additions. + +The agent runs as a [Goose](https://goose-docs.ai/) session. Goose provides: + +- The chat loop, multi-turn history, and streaming +- The OpenRouter provider that routes to Nous Hermes +- The built-in `developer` extension (shell + file editor — same surface Claude + Code has) +- A remote MCP extension pointed at `mcp.hubris.network` for read-only + homelab context (`list_lxcs`, `tail_log`, `search_docs`, etc.) + +The persona is `/opt/homelab-context/HERMES.md`, symlinked as Goose's global +`.goosehints` so it's injected into the system prompt on every session. + +## Prerequisites + +| Requirement | How | +| --- | --- | +| Standard enrollment complete (`homelab whoami` works) | [agent-enrollment.md](./agent-enrollment.md) | +| `secrets/openrouter-api-key.yaml` exists with a real `sk-or-...` value | See "Seeding the OpenRouter key" below | +| The host's `age_pubkey` is on the openrouter-api-key.yaml sops rule | `homelab client add --finalize-pubkey --with-hermes` | + +## Onboarding flow + +```bash +# 1. On hubris (or any enrolled client): reserve the inventory entry. +homelab client add new-machine + +# 2. Join new-machine to Netbird (setup-key or OIDC). + +# 3. On new-machine: bootstrap with --with-hermes. +TOKEN=... # gitea PAT, read:repository +curl -fsSL -u "dtoro:$TOKEN" \ + https://git.hubris.network/dtoro/Homelab-Docs/raw/branch/main/bootstrap.sh \ + -o /tmp/bootstrap.sh +sudo HOMELAB_GITEA_TOKEN=$TOKEN bash /tmp/bootstrap.sh --with-mcp --with-hermes + +# 4. Back on hubris: finalize the age pubkey AND grant the Hermes secret. +homelab client add new-machine \ + --finalize-pubkey age1... \ + --with-hermes + +# 5. Wait ≤5 min for sync, then on new-machine: +hermes "what LXCs are running?" +``` + +The bootstrap `--with-hermes` flag does five things, all idempotent: + +1. Downloads the latest Goose binary into the operator's `~/.local/bin/goose` + (upstream installer) and symlinks `/usr/local/bin/goose` to it. +2. Symlinks `/opt/homelab-context/bin/hermes` → `/usr/local/bin/hermes`. +3. Symlinks `/opt/homelab-context/HERMES.md` → `/root/HERMES.md` (Linux) or + `/etc/HERMES.md` (macOS) for `cat`-as-operator convenience. +4. Drops `~/.config/goose/config.yaml` pinning the provider, model, and + extensions (preserves any keys the operator added by hand). +5. Symlinks `~/.config/goose/.goosehints` → HERMES.md, so the persona is + injected as the system prompt on every session. + +## Seeding the OpenRouter key + +The first time anyone enrolls with `--with-hermes`, the encrypted file +`secrets/openrouter-api-key.yaml` contains a placeholder. On hubris (or any +existing recipient): + +```bash +sops secrets/openrouter-api-key.yaml +# editor opens; replace api_key value with the real sk-or-... key, save, close. +git -C /opt/homelab-context add secrets/openrouter-api-key.yaml +git -C /opt/homelab-context commit -m 'openrouter-api-key: seed real key' +git -C /opt/homelab-context push +``` + +Until this step happens, `hermes …` exits with `openrouter-api-key.yaml still +contains the placeholder`. Subsequent enrollees get the real key automatically +via `--with-hermes` (which adds them as a sops recipient on +`secrets/openrouter-api-key.yaml`). + +## Granting the OpenRouter key to an already-enrolled host + +If a host was enrolled without `--with-hermes` and you want to add it later: + +```bash +# On hubris: +PUBKEY=$(homelab whoami --hostname | grep age_pubkey | awk '{print $2}') +homelab client add --finalize-pubkey "$PUBKEY" --with-hermes +``` + +`--finalize-pubkey` is required by the existing flow even when the pubkey is +unchanged — it's also the trigger that runs the sops grant. + +After ≤5 min sync the host can decrypt the key. Bootstrap doesn't need to +re-run; only the secret recipient list changed. + +## Verifying + +```bash +homelab whoami # standard enrollment OK +homelab secret openrouter-api-key | head -c 8 # decrypts (prints `api_key:`) +which goose && which hermes # binaries present +goose info -v # provider/model wiring sane +hermes "what LXCs are running?" # interactive Goose session + +# Non-interactive smoke test: +echo "List the homelab MCP tools you have available" | hermes +``` + +## Configuration + +The bootstrap-managed keys in `~/.config/goose/config.yaml`: + +```yaml +GOOSE_PROVIDER: openrouter +GOOSE_MODEL: nousresearch/hermes-4-405b +GOOSE_MODE: smart_approve # asks before destructive tool calls +extensions: + developer: + type: builtin + bundled: true + enabled: true + name: developer + timeout: 300 + homelab: + type: streamable_http + enabled: true + name: homelab + uri: https://mcp.hubris.network/mcp + timeout: 60 +``` + +Override via env on a single bootstrap run: + +```bash +HOMELAB_HERMES_MODEL=nousresearch/hermes-3-llama-3.1-405b \ +HOMELAB_HERMES_MCP_URI=https://mcp.hubris.network/mcp \ + sudo bash /tmp/bootstrap.sh --with-hermes +``` + +Any keys you add by hand (e.g. `GOOSE_TEMPERATURE`, extra `extensions.*`) are +preserved across re-bootstraps — the merge only overwrites the keys it manages. + +## Tool permissions + +`GOOSE_MODE: smart_approve` is the bootstrap default: Goose runs read-only +shell commands without prompting and asks for confirmation before destructive +ones. To make the agent fully unattended (e.g. for scheduled jobs), set +`GOOSE_MODE: auto` in `~/.config/goose/config.yaml`. To require confirmation on +every tool call, use `approve`. See +[goose-permissions](https://goose-docs.ai/docs/guides/managing-tools/goose-permissions/). + +## Troubleshooting + +| Symptom | Cause | Fix | +| --- | --- | --- | +| `hermes: could not decrypt secrets/openrouter-api-key.yaml` | Host isn't a recipient on the sops rule | `homelab client add --finalize-pubkey --with-hermes` from hubris | +| `hermes: openrouter-api-key.yaml still contains the placeholder` | No real key has been seeded yet | See "Seeding the OpenRouter key" above | +| Goose hangs on first `hermes` invocation with no output | Goose's interactive `configure` ran on first launch and is awaiting input | Re-run; the installer is supposed to skip it (CONFIGURE=false). If it persists, run `goose configure` once manually in a real terminal to commit the config. | +| `homelab` extension fails to connect / no MCP tools listed | MCP server still runs SSE-only; Goose requires `streamable_http`. See follow-up #1 below. | Either: (a) migrate the FastMCP server to streamable_http (one-line change in `mcp/server.py` — `mcp.run(transport="streamable_http")` — then redeploy), or (b) accept that the agent works via the developer extension alone (shell + `homelab` CLI cover everything MCP would). | +| `goose: command not found` after bootstrap | Upstream installer dropped binary in `~/.local/bin/` but `/usr/local/bin/goose` symlink didn't land | Re-run bootstrap with `--with-hermes`; the symlink step is at the end of the install block. If still missing, `ln -sfn ~/.local/bin/goose /usr/local/bin/goose` manually. | +| Tool calls hit OpenRouter rate limits | One shared key across many hosts | Future: per-host keys; for now, see the rate-limits guide referenced in `goose info -v`. | + +## Cross-references + +- [agent-enrollment.md](./agent-enrollment.md) — base client onboarding the + Hermes flow assumes is done. +- [`HERMES.md`](../HERMES.md) — the persona the Hermes agent reads on every + session start (via `~/.config/goose/.goosehints`). +- [`bin/hermes`](../bin/hermes) — the wrapper that decrypts the OpenRouter key + and execs `goose session`. +- [`bootstrap.sh`](../bootstrap.sh) — the `--with-hermes` flag's install block. + +## Follow-ups + +1. **Migrate the MCP server to streamable_http.** Goose 1.x deprecated SSE + (`"SSE transport is no longer supported - kept only for config file + compatibility"` in `crates/goose/src/agents/extension.rs`). Our FastMCP + server at `mcp/server.py:336` still calls `mcp.run(transport="sse")`. Until + that's changed, the `homelab` MCP extension in Goose will fail to connect. + The developer extension (shell + edit) covers most ops without it; this is + a polish item, not a blocker. +2. **Per-host OpenRouter keys** for billing attribution. Today all Hermes + hosts share one key. +3. **Pin the model version** rather than tracking `nousresearch/hermes-4-405b` + directly — OpenRouter periodically rotates the underlying weights. +4. **Local-inference fallback** (ollama / vllm) once the homelab has a GPU + node. The wrapper, persona, and MCP wiring stay unchanged; only + `GOOSE_PROVIDER`/`GOOSE_MODEL` change. + +## Changelog + +### 2026-05-31 — initial page +Captures the Hermes-on-Goose onboarding flow added in the same commit as +`bootstrap.sh --with-hermes`, `bin/hermes`, the sops rule for +`secrets/openrouter-api-key.yaml`, and the `homelab client add --with-hermes` +extension. MCP streamable_http migration is queued as follow-up #1. diff --git a/secrets/openrouter-api-key.yaml b/secrets/openrouter-api-key.yaml new file mode 100644 index 0000000..d592db4 --- /dev/null +++ b/secrets/openrouter-api-key.yaml @@ -0,0 +1,30 @@ +api_key: ENC[AES256_GCM,data:ps6aTT939SWpgyUoMMIj6YsJG3UnJFBdRKuESpoMeyphB9QnrkqSb5Y/kXI=,iv:6KxZTpPUR2GmPzr5opMP4Wp/yPIQqE9AWtAO5xBDGTI=,tag:qfRt1AlQ97PeHMsGmXnqsQ==,type:str] +sops: + kms: [] + gcp_kms: [] + azure_kv: [] + hc_vault: [] + age: + - recipient: age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6 + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBUZEU0QWtxb0RMMkNqTHI3 + d25ONHF2Z2F6L0c4VWd6amMvYkQ0NU5YeEh3ClpBSEt5bXNJVmF5Q2pzNGdzVWVC + MzFDSTk2YlpyWkxIdDg0RmFxR21EMzgKLS0tIE1GRW52Wk10eXppM05XSDMrc3RH + eWQ5TFR0R29RWUxsbVhlWm1TVkFvb0UKwv6OafGdw06J21tQoUlvNI0aLVvl2CVm + nwbltuhQvWdFPVBRP/Cx0eK7Vh80tJrnOfXeGpNVgiCs2lpxBlUR2g== + -----END AGE ENCRYPTED FILE----- + - recipient: age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6 + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBMZVdubENvWkxGWmNnVks0 + NFJYQVBKMnJJZ3FEUDVJWXl2WitMTDFYdEd3CmJQaSt4bTZyUVlBZHgybXRhbWky + UGIzNkwzUzNZaWN3WmNKSUkxQkl0OTAKLS0tIENyNlphYmVRV3BUNU1FQzd0QStN + V0JLNU5RT09RVjd2OTJUTWMvek9NaTQK0R0OfQnm7AhIeroefldUNDsgDHX1cDmk + GybdGEoM8j8Is8GLc6Gk+yQmz7yNvVfYBkaaFsH3Crz3HupuyXI59g== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2026-05-30T23:08:58Z" + mac: ENC[AES256_GCM,data:68i7qPK7zJXDspmVYfTVJGd141ldXeiPEHK/tsX3p9QHBSP8otXwAzaEqcjNG83dXhrse9CRzzyJmrLm7EO714hZyHKh6MKZb/BOuiwnMCZse0qbK55ghkzTQLMkeSuIM2KsN396mlH/Ehd/hQV6oC6OPq2RQUs/aJt55ESutLc=,iv:kGPKf9e6w1vz7soEyDL7mGQCBkrxiG0Wvm7FHEYp3jc=,tag:zHd7qoIx9Fei2Aabgsztuw==,type:str] + pgp: [] + unencrypted_suffix: _unencrypted + version: 3.9.4 From 543745cad98af55bdac93c9fdda139c689740468 Mon Sep 17 00:00:00 2001 From: dtoro Date: Mon, 1 Jun 2026 22:48:02 +0200 Subject: [PATCH 02/25] network: retire Slate AX; add SODOLA switch migration plan and network topology doc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - infrastructure/network.md: permanent topology reference — VLAN 1 (main LAN) + VLAN 10 (homelab 192.168.8.0/24), Fritz!Box 7590 as VLAN router, SODOLA 5-Port 2.5Gbit as homelab switch - plans/2026-06-01-slate-ax-to-sodola-migration.md: full migration runbook (pre-flight, Fritz!Box second-network config, SODOLA VLAN table, cutover procedure, verification commands). Gateway 192.168.8.1 is unchanged — no LXC/VM config edits needed. Fritz!Box DHCP for VLAN 10 hands out 192.168.8.2 (Technitium CT 107) as DNS, closing the outstanding item in dns.md. - plans/index.md: establishes plans/ convention (Planned → In Progress → Done) - CONTRIBUTING.md: plan page template + linking discipline for plans Co-Authored-By: Claude Sonnet 4.6 --- CONTRIBUTING.md | 25 ++++ infrastructure/network.md | 71 ++++++++++ ...2026-06-01-slate-ax-to-sodola-migration.md | 131 ++++++++++++++++++ plans/index.md | 16 +++ 4 files changed, 243 insertions(+) create mode 100644 infrastructure/network.md create mode 100644 plans/2026-06-01-slate-ax-to-sodola-migration.md create mode 100644 plans/index.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 30fb393..1cfd2d7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -67,6 +67,30 @@ Links to nodes that host or depend on this. ## Changelog ``` +### Plan (`plans/YYYY-MM-DD-slug.md`) + +```markdown +# YYYY-MM-DD — + +## Goal +What this change achieves and why. + +## Current topology / state +Diagram or description of what exists now. + +## Target topology / state +What it looks like after. + +## Pre-flight checklist + +## Step-by-step procedure + +## Verification + +## Post-migration +Changelog entries to write, index status to update. +``` + ### Investigation (`investigations/YYYY-MM-DD-slug.md`) ```markdown @@ -89,6 +113,7 @@ Links to nodes that host or depend on this. - Every container page links to every cross-cutting page it participates in. - Every cross-cutting page lists the nodes that participate. - Every investigation links to the nodes it implicates *and* gets back-linked from each node's changelog. +- Every plan links to the infrastructure pages it affects. When done, update the plan's status in `plans/index.md` and write changelog entries on affected node pages. ## Changelog hygiene diff --git a/infrastructure/network.md b/infrastructure/network.md new file mode 100644 index 0000000..f94fbff --- /dev/null +++ b/infrastructure/network.md @@ -0,0 +1,71 @@ +# Network + +Physical and logical network topology for the homelab. + +## Why + +The homelab runs on a dedicated subnet (`192.168.8.0/24`) isolated from the main household LAN (`192.168.178.0/24`). Isolation is enforced at the Fritz!Box level: homelab devices can reach the internet and main LAN can reach homelab services via explicit port forwards, but main LAN cannot initiate arbitrary connections into the homelab subnet. + +## Hardware + +| Device | Role | +|---|---| +| Fritz!Box 7590 | Main router / ISP gateway, VLAN router between subnets | +| SODOLA 5-Port 2.5Gbit Managed | Homelab switch, all ports in VLAN 10 | + +## VLANs + +| VLAN | Subnet | Gateway | Devices | +|---|---|---|---| +| 1 (native) | 192.168.178.0/24 | 192.168.178.1 | Household LAN — laptops, phones, APs | +| 10 | 192.168.8.0/24 | 192.168.8.1 | Homelab — Proxmox, all LXCs/VMs | + +## Topology + +``` +ISP + └── Fritz!Box 7590 + ├── VLAN 1 192.168.178.0/24 (main LAN) + └── VLAN 10 192.168.8.0/24 (homelab) + │ + └── SODOLA 5-Port 2.5Gbit + ├── Port 1 uplink → Fritz!Box + ├── Port 2 hubris (Proxmox, 192.168.8.77) + ├── Port 3 [device] + ├── Port 4 [device] + └── Port 5 spare +``` + +## DHCP + +Fritz!Box serves DHCP for both VLANs. For VLAN 10 homelab: +- Range: `192.168.8.100 – 192.168.8.240` +- DNS: `192.168.8.2` (Technitium on [CT 107](../containers/107-dns.md)) + +Most homelab hosts use static IPs below `.100` — DHCP only assigns to new/transient devices. + +## DNS + +Split-horizon DNS for `*.hubris.network` served by Technitium on [CT 107](../containers/107-dns.md) at `192.168.8.2:53`. See [dns.md](dns.md) for full detail. + +## Port forwarding + +All inbound port forwards are on the Fritz!Box directly. The old double-hop (Fritz!Box → Slate AX → homelab) was eliminated when the Slate AX was retired. Target IPs are homelab addresses (`192.168.8.x`). + +## Remote access + +- **NetBird mesh** — primary path for remote administration. Authenticated via [Authentik on the VPS](../vps/). +- **Tailscale** — legacy, being phased out. See [mesh.md](mesh.md). + +## Related + +- [DNS](dns.md) — split-horizon config and entry list +- [Ingress](ingress.md) — public entry points via VPS traefik +- [Mesh](mesh.md) — NetBird / Tailscale VPN overlay +- [hosts/hubris.md](../hosts/hubris.md) — Proxmox host +- [CT 107 — dns](../containers/107-dns.md) — Technitium DNS server + +## Changelog + +### 2026-06-01 — Slate AX retired; SODOLA switch added; Fritz!Box becomes homelab gateway +Replaced the GL.iNet Slate AX sub-router with the SODOLA 5-Port 2.5Gbit managed switch. Fritz!Box 7590 now routes VLAN 10 (`192.168.8.0/24`) directly — no more double-NAT. Gateway `192.168.8.1` is unchanged so no LXC or VM needed reconfiguration. Fritz!Box DHCP for VLAN 10 now hands out `192.168.8.2` (Technitium) as DNS, resolving the outstanding "update router DHCP DNS" item. See [migration plan](../plans/2026-06-01-slate-ax-to-sodola-migration.md). diff --git a/plans/2026-06-01-slate-ax-to-sodola-migration.md b/plans/2026-06-01-slate-ax-to-sodola-migration.md new file mode 100644 index 0000000..90b8c83 --- /dev/null +++ b/plans/2026-06-01-slate-ax-to-sodola-migration.md @@ -0,0 +1,131 @@ +# 2026-06-01 — Slate AX → SODOLA Migration + +**Status:** Planned +**Hardware:** SODOLA 5-Port 2.5Gbit Managed Switch replacing GL.iNet Slate AX +**Router:** Fritz!Box 7590 + +## Goal + +Remove the Slate AX sub-router. It adds double-NAT, double port-forward complexity, and a second DHCP/gateway hop. Replace with the SODOLA switch so Proxmox and homelab devices attach directly to the Fritz!Box—without changing any of their IPs. + +## Current topology + +``` +ISP + └── Fritz!Box 7590 (192.168.178.1) + └── GL.iNet Slate AX WAN: 192.168.178.x LAN: 192.168.8.1 [NAT] + ├── hubris (Proxmox) 192.168.8.77 + ├── + all 16 LXCs on vmbr0 bridged to the same subnet + └── other homelab devices +``` + +## Target topology + +``` +ISP + └── Fritz!Box 7590 + ├── VLAN 1 192.168.178.0/24 (main LAN — laptops, phones) + └── VLAN 10 192.168.8.0/24 gateway 192.168.8.1 (homelab, no NAT) + │ + └── SODOLA 5-Port 2.5Gbit + ├── Port 1 uplink → Fritz!Box LAN port + ├── Port 2 hubris (Proxmox) 192.168.8.77 + ├── Port 3 [homelab device] + ├── Port 4 [homelab device] + └── Port 5 spare +``` + +Fritz!Box takes over `192.168.8.1` — the same gateway IP the Slate AX used. No static IPs or gateway entries change on any LXC or VM. + +See [network architecture](../infrastructure/network.md) for the permanent topology reference. + +## Pre-flight checklist + +- [ ] Note Slate AX gateway IP on the homelab subnet (expected `192.168.8.1`) +- [ ] Note Slate AX upstream DNS setting (what it forwards to) +- [ ] List all port forwards currently on the Slate AX → move these to Fritz!Box +- [ ] List all port forwards on Fritz!Box that point at the Slate AX WAN IP → collapse these into direct homelab-IP rules +- [ ] Confirm Fritz!OS version supports "second IP network" (Fritz!OS 7.x required) +- [ ] SSH into hubris: verify `grep gateway /etc/network/interfaces` → `192.168.8.1` +- [ ] SODOLA web UI accessible and ports configurable before physical move + +## Fritz!Box configuration + +**Path:** Home Network → Network → Network Settings → IPv4 Addresses → Add IP network + +| Field | Value | +|---|---| +| IP address | `192.168.8.1` | +| Subnet mask | `255.255.255.0` | +| DHCP range | `192.168.8.100 – 192.168.8.240` | +| Assign to | LAN port that connects to SODOLA | +| Network isolation | Enabled (blocks main LAN from initiating into homelab) | +| DNS for DHCP clients | `192.168.8.2` (Technitium on [CT 107](../containers/107-dns.md)) | + +After creating the network, move any port forwards from the Slate AX into Fritz!Box → Internet → Permits (target IPs are now directly reachable on `192.168.8.x`). + +## SODOLA configuration + +All ports in VLAN 10, untagged (simple access-port setup): + +| Port | PVID | Role | +|---|---|---| +| 1 | 10 | Uplink → Fritz!Box | +| 2 | 10 | hubris (Proxmox) | +| 3 | 10 | homelab device | +| 4 | 10 | homelab device | +| 5 | 10 | spare | + +## Proxmox / LXC changes + +If the Slate AX gateway was `192.168.8.1`: **no changes needed**. Fritz!Box takes the same IP. + +If Slate AX used a different gateway (e.g. `.254`), update: +```bash +# on hubris +nano /etc/network/interfaces # change gateway line +ifreload -a +# for each LXC with a static gateway +pct config <id> # check net0: line for gw= +pct set <id> --net0 name=eth0,bridge=vmbr0,ip=<ip>/24,gw=192.168.8.1 +``` + +## DNS after migration + +Technitium ([CT 107](../containers/107-dns.md)) at `192.168.8.2` continues to serve split-horizon DNS for `hubris.network`. The Fritz!Box DHCP server for VLAN 10 hands out `192.168.8.2` as the DNS server. This fixes the "update router DHCP DNS from dead .180 → .2" outstanding item in [dns.md](../infrastructure/dns.md). + +## Cutover procedure + +1. Configure Fritz!Box second IP network (VLAN 10 / `192.168.8.1`) — verify DHCP works via a test laptop on the target port. +2. Configure SODOLA port VLAN assignments. +3. Connect SODOLA port 1 → Fritz!Box target LAN port. +4. Move hubris ethernet → SODOLA port 2. Proxmox should stay reachable immediately (same gateway IP). +5. Move remaining homelab devices to SODOLA ports 3–5. +6. Decommission Slate AX. + +## Verification + +```bash +# internet from Proxmox +ssh proxmox "ping -c3 1.1.1.1" + +# Fritz!Box main LAN reachable (routing between VLANs works) +ssh proxmox "ping -c3 192.168.178.1" + +# split-horizon DNS via Technitium still resolves +ssh proxmox "dig @192.168.8.2 +short git.hubris.network" +# expected: 192.168.8.175 + +# Caddy reverse proxy responds +curl -sk https://git.hubris.network | head -5 + +# Authentik on VPS still reachable +curl -sk https://auth.hubris.network/if/flow/default-authentication-flow/ | head -5 +``` + +## Post-migration + +- Update [network.md](../infrastructure/network.md) topology to reflect new state. +- Add changelog entries to [hosts/hubris.md](../hosts/hubris.md) and any affected container pages. +- Update status in [plans/index.md](index.md) to `Done`. +- If anything went sideways, open an investigation in `investigations/`. diff --git a/plans/index.md b/plans/index.md new file mode 100644 index 0000000..6268434 --- /dev/null +++ b/plans/index.md @@ -0,0 +1,16 @@ +# Plans + +Pre-flight runbooks for planned changes that haven't happened yet. Once executed, move the outcome to `investigations/` (if anything interesting happened) or just a changelog entry on the affected node pages. + +## Index + +| Date | Title | Status | +| ---- | ----- | ------ | +| 2026-06-01 | [Slate AX → SODOLA managed switch migration](2026-06-01-slate-ax-to-sodola-migration.md) | Planned | + +## Conventions + +- File name: `YYYY-MM-DD-<slug>.md`. Use the *target* date if known, otherwise the planning date. +- Status: `Planned` → `In Progress` → `Done` (update index entry as it moves). +- When done: add a changelog entry on every affected node page, then update status to `Done` here. If things went sideways, open an investigation. +- Plans are append-only once execution starts — don't rewrite pre-flight intent after the fact. From 76bbe99798387ec275e1412cfa4f34fb52c73013 Mon Sep 17 00:00:00 2001 From: root <root@hubris.local> Date: Mon, 1 Jun 2026 22:49:01 +0200 Subject: [PATCH 03/25] client-add: mac-mini (finalize age_pubkey + grant shared + hermes secrets) --- .sops.yaml | 15 +++++++---- hosts/mac-mini.yaml | 1 + inventory.yaml | 2 +- secrets/gitea-pat.yaml | 39 ++++++++++++++++++----------- secrets/hello.yaml | 39 ++++++++++++++++++----------- secrets/netbird-authentik-oidc.yaml | 39 ++++++++++++++++++----------- secrets/openrouter-api-key.yaml | 29 +++++++++++++-------- secrets/turn-shared-secret.yaml | 39 ++++++++++++++++++----------- 8 files changed, 127 insertions(+), 76 deletions(-) diff --git a/.sops.yaml b/.sops.yaml index 3976197..ef039fd 100644 --- a/.sops.yaml +++ b/.sops.yaml @@ -23,7 +23,8 @@ creation_rules: age: >- age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6, age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0, - age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6 + age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6, + age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs - path_regex: ^secrets/gitea-pat\.yaml$ # Write-scoped Gitea PAT (dtoro user). Same recipient list as hello.yaml @@ -32,7 +33,8 @@ creation_rules: age: >- age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6, age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0, - age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6 + age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6, + age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs - path_regex: ^secrets/gitea-tokens\.yaml$ # Workstations only. @@ -52,7 +54,8 @@ creation_rules: age: >- age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6, age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0, - age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6 + age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6, + age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs - path_regex: ^secrets/netbird-authentik-oidc\.yaml$ # Authentik OIDC client secret for the netbird-dashboard provider. @@ -61,7 +64,8 @@ creation_rules: age: >- age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6, age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0, - age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6 + age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6, + age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs - path_regex: ^secrets/openrouter-api-key\.yaml$ # OpenRouter API key consumed by the `hermes` wrapper (bin/hermes) when @@ -71,5 +75,6 @@ creation_rules: # See operations/hermes-agent.md. age: >- age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6, - age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6 + age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6, + age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs # webhook noop 2026-05-20T18:16:57+02:00 diff --git a/hosts/mac-mini.yaml b/hosts/mac-mini.yaml index ddd5ad3..a608ac3 100644 --- a/hosts/mac-mini.yaml +++ b/hosts/mac-mini.yaml @@ -17,5 +17,6 @@ ssh: user: dtoro notes: - Only macOS in the fleet. Bootstrap uses launchd. +age_pubkey: age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs mcp_endpoint: https://mcp.hubris.network/sse secrets_issuance_endpoint: https://secrets.hubris.network/issue diff --git a/inventory.yaml b/inventory.yaml index c8ef328..072eb98 100644 --- a/inventory.yaml +++ b/inventory.yaml @@ -341,7 +341,7 @@ hosts: user: dtoro notes: - Only macOS in the fleet. Bootstrap uses launchd. - age_pubkey: '' + age_pubkey: age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs ludo-mini: kind: workstation os: linux diff --git a/secrets/gitea-pat.yaml b/secrets/gitea-pat.yaml index 0c2117e..3b8dd91 100644 --- a/secrets/gitea-pat.yaml +++ b/secrets/gitea-pat.yaml @@ -13,29 +13,38 @@ sops: - recipient: age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6 enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBQL1FueldvT09lV011Q0hz - MlJ1YkxIYXZDNDVQQWo1MFQrRlNBcXNIYVNRCllXYVZrczdBNHdPemJybUVlQkVl - d0hKUTVsTyszOUFrY1BQQllSM2JWa2sKLS0tIDFISUw3T1lLYm10YXZaeFlTMWVj - ZHFWN0ZQTlhIc0pGMThoWWViN2lmVUUKsWRBHqoRVmY2Clcro5aKhQj9yX5W4Kh3 - arC5FKBoZ/8KlcS7zxC9vIISOqlQu7MCKA8L//HX/gkfcz77aS6t1w== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXaWJhdFAxdThGSVZRN2I1 + VjE1djFtdjdnSFVJU3lMZC9YSzNSeGVLaVJBCjlId3pvcFRadUdwamxaa00zNUVl + VUJFaDRRLzdSNDNVekR3eWdmREh3eUEKLS0tIEVMMmh2eUlSL0FhRzNnZXJyWk9I + MEY3dlVDQktRR1VUajM4WmVwYS80TVUKA0bkns4IE093PvF5Ka3HNWmi+Htqs66H + BBEAETnQZOdM/Ca+oySDnhLYYT4rD46m4d3H0YQfXQhlVk/h9CTshQ== -----END AGE ENCRYPTED FILE----- - recipient: age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0 enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBFTVB1eWh5M3RUVFRubUFE - OTRrbWRnUDhsVGlLZkFWckRXZ08xTkxCMlVNCnFHcU9aMklma3RJZnZ5OHUxQVYx - SWFZMEJSNHExT0lsdW9FNkx0T3ZNQ0UKLS0tIEpseGdCbTJudUNWUUJGYUg1STJV - Qm9QU3Z2NHhiRWh6VnY5cDRRQVk2ZUEKktovOMBi2eT4Hxwm8i8gy4GYPpOZ3nIy - cDM6yBpUQMybiY3jMji41ELg12vF2kQ286W6lQKURdl9yYoGz0SfqQ== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5bVZma09OVWkrVmtVNEda + MnRxdDZuRTV3RmhieTB2ZXVTcjY2VWtTOW5rCkw2STNWTHluRGZhT3B4TmtFWHUv + L2hyd3QwMW1iaHRCRWMycmxHVW5qMlEKLS0tIHNrTjdYb3NCdU05WjR3RFdvdFBR + Z3NRRVRRcnBhdXVmRmprbk1iL0lWS28KTvN7Z3VyXKCnbJBD9N+FtC5UDCxofPIU + WERW/eV7+2F6SeClSu7iE+pDMbjiKrFBVKBAU8OA8yd8VmcnD8IWdw== -----END AGE ENCRYPTED FILE----- - recipient: age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6 enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBHd0NhOC9FWmVSL0ExKzIz - L1hiU3RhKzh4SzFCNURhWFAxZTF4QWJIdmhBClQ4SzBEY2VweUR5UnJJSkk1OUVj - MWp5WUtQcE0yNFh5R0JoTGpUaWJiU0UKLS0tIDFQbDZoUXlPSURndTNEN2d0TjJS - djVvR2hGSllUNlZJNURrMG43bmZlU0EKl7SfN6/JGbzDA3YVrRnua5QK8xEdkWoB - ey5Yf4Zg0obqYIS5SX8QC/YtoeMANVkKtqVTOVE2PYLJtrC6l7vKxQ== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBublRwajRDeUtmdmtpN3pj + VVE0VWJhVWRXeE1UM0xHdEJKUVhHVWpNTG00CnZjNUg3ZkUvcFZTRHc4UDMrOVhV + QTNPMmphTkhxZjgzNXN6REtTTTZUU00KLS0tIEk0MXFBNS8wVmpMZFhoYUpsOGlG + ZE1OaXdhZVVGQXUrdUhJT09UU21DNkUK4DcLYPqf3ojRotaa95KIUhKYQwWorsum + IletEBZtyJdOJgpwN/eZLe12a4E5thI245jSjQlw2B8RKFqOCDyASw== + -----END AGE ENCRYPTED FILE----- + - recipient: age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBPVEdGR0VwUjk3MUROOHRJ + aXdFMTFDRFZBek5FSWJSbWJDU2F2ODFyTVFzCndTbStrQmFSSWJwbFc0M1Nia2FS + eGZEMGsrYkNnT3M2aEhOS0IzOVNOVncKLS0tIENtOEx4SVJQTFcxR3NIM0NHVDI5 + Z1RIMWRBNGhCOFF4MkdHRlpya25ybGMKsyLYsmqxuXvJ4ZF97Jh5D8BoepehSdKi + yGzLaaQo0gW/wu7n0fq7S7HhbiTcPZ8lQboVvhYWU2lTx1p8npUsAQ== -----END AGE ENCRYPTED FILE----- lastmodified: "2026-05-20T16:24:18Z" mac: ENC[AES256_GCM,data:d3qcjEqpPIFeut634ImvISJLdit0MQWcdFYomrsqriqJ0NUKdyq3XCk85+vvPvDWikB4WEApk6HMXssSY8mhDci95q5Ssmr+JRAhLebMZjs9yXUf9A7vXpNFswmdldu3CKBiDrhm2GE08qUfsCkcf3jjihEqsfHhxMuWSd5fZpU=,iv:VZgvZ3bBArRChgRX38U6/43XoX5aTnts7Kd4S9spBPk=,tag:/5oOw5yvulvFT6KXv2zj0g==,type:str] diff --git a/secrets/hello.yaml b/secrets/hello.yaml index 5769f8c..3c12c7c 100644 --- a/secrets/hello.yaml +++ b/secrets/hello.yaml @@ -9,29 +9,38 @@ sops: - recipient: age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6 enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCbWhQa2RreXl3Ni9zZExQ - dzB5Q1hSZzJSb1NlaEx3bEFCK2x6Ty82ZEJ3Cng3Q1NmKzhpdmpvTWFOL0lUb3F6 - emtvNCt3elQwTndoNzRIM2FoZTlpTVUKLS0tIER6STdwVUtnL0lreFh3Z09FYlBo - azJuWjVuNGJURTNPYU1aWWZGM0dpc2cKFM8AjHFg68fedY0Xrg2U69+Qxc5Pgn6f - QipSsaXrCohzNKieqCG9maKuLcGrgMQ1OJqZQm+hfit6CRKUmVHRkw== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBUanM5MzFZYnkxVHVHSlYy + U1Q1ejNJb3ZORklKM2xjK0p2cytRU0NNRUZJClBTUDJ5dFM2eUFjV0t1YzBCcXcv + cmE3czJST1RuMHJVN0tmVTZNdGNRS1kKLS0tIFUydktvK0tBU2U3czRhWHVySGZR + ck9KTVR6RS9pOHQ4WkZneG1Tbk5OSWMKwOQ+CWkuLSqfhle9fgDw4XXIp0ojZssw + 9YWK/suEAb6u9nzbw7zuEmZxhhDV0Y61UAeSSbSmygy7MD7dvxrvgQ== -----END AGE ENCRYPTED FILE----- - recipient: age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0 enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB2TVpWeC9FcmUxckNadmtK - d3BwZnExR3VqeXBQSGhNdStPVXM2MkdjQXhBCkpHMmxPZ3JWaVVvUGwrVjZadXlR - V3VaR2l5Zm9zb1VrREphM09RMU5mR0UKLS0tIGlYZm5XbTA2WUcvWU9FWlZYSWNZ - L1JYYmJzLyt6bzJqdlk2TjYvNUdVVTgKlj1b9zQ9cBG3YQI2t4fjz3t9ySBxQ15w - qxcyGspEQ9F+4OtoRQP0FCfmJIBNxvAoT2I5TjspH6dl2o1ID+332Q== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCbm5MOG1Bd0FZbVZTbUU1 + VGpGVWNnUk03RURPNURNNjdQejY2WkxkNzBRClpuMGFaQzZhaENJckZvbUI3VmUy + ekV0eFMza1p4S2w5L0hvNndjN3RpdTQKLS0tIDRTdExxTlZQK0FKV294dnh4dmd4 + a21yK0VabzhOcDhRajlxbVE1dTdtcWcKQorqkwUs7lppaDJuCDn+KtH+76xPIvsr + 9axkrDLumeY9LtDhunlaEpax7zdvvAiC4DzJdbJX0LgFCaurtgubOA== -----END AGE ENCRYPTED FILE----- - recipient: age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6 enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBFa1NwSGlhUzgxdjI2V0Ew - VVNRS1o2L00wM0QvdTFXelFLVnBrQ2ZHRHprCks0Z2ZQbzdQNG5hVFlCZXFrSG5B - cG5Qb2JUbnk4ekdJSER2MU5NRGZ6NEUKLS0tIDF4cjNzWnBLaXJNS1RHbVozVUtp - bTkzM21jNndEdDV0RmtxSVZ5SkdQcGMK+MZNNAfnnXq7VjIvaDtJEaImk0mecErp - gzV3b0MOKc2eTiuaWRNH1yAhJOFTK9iQfC7dDUI8npE+zLOgpLEZ9w== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBaS1JrL3lpa1c4QUwxWlZT + VWQ1ZDVPVjN6QTdFNGQzNkV5WllOK3dYWHhzCllTbVdzRVFTbDZ4TU9PZWpKNnJu + cWJ6YWs5amVsR2xZZFpaQlErOXBKZVEKLS0tIFFzVG9QWmdNWGl5UEkxaXZvMUxx + SExXL1gxNzVud2JrZ04zTnI4cUlNSVEKPF2zoSnYEt/zeG8QW1454Mcr8u8JTCl7 + jDyghcRw94enbPbA43zsKGn6QALQ40PfXmZ/MlEbdnf6U3zI7zvygQ== + -----END AGE ENCRYPTED FILE----- + - recipient: age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAwMlVSajFiT2loclFzeEQ5 + ekxqUjBkSEh5M2FJMXA2VGkycllYa1R1Qm40Clh0L0RwSzZqeXlQVXMvRVlsZ05L + RHkyMlJlc1BmV0tjQk1uaWJLemR2cmMKLS0tIDlXT1dhRkpUdDRpRkJhL2NHSjN5 + QU5Pc25lSUV4OWROZENXQzB4Y0o2Y1kKzhOY5jN4gi+u5tl9rAHGRb8Bh9DESl1K + qNiD+wauApal8iRrIkGdGsrWrWThSwy7vjnUsoB8JDrBaqLj2usuFA== -----END AGE ENCRYPTED FILE----- lastmodified: "2026-05-20T14:39:14Z" mac: ENC[AES256_GCM,data:CAoIW1sJkIbNoXMaVlXwfHdREzATNqDKpSN5ecaSiRARThB0/tcLNbkazXKyV1Rx5b+b3s+yCpBzwmNnH6ytaCzG5pKPcnfd6YJq7C5rm/EQHyaVGrPdBsSfqiz4yBXKClej//v3+qkD5Ls2PMppv5KmqigOKNQC/ot3ht2c7lM=,iv:eOmDDclksr9f9CDu6dJprSFwlWuoggzVFjyfqWf5Uhg=,tag:IU3sMlIi92ohM86zOIJdAg==,type:str] diff --git a/secrets/netbird-authentik-oidc.yaml b/secrets/netbird-authentik-oidc.yaml index cb04de5..190ef0c 100644 --- a/secrets/netbird-authentik-oidc.yaml +++ b/secrets/netbird-authentik-oidc.yaml @@ -8,29 +8,38 @@ sops: - recipient: age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6 enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzVVZIQzlPak4xTVVPMm9w - M2tFbTFoQmpRUGFETjF1ZTQ3ajlEdGxWQmtZCkdkZzdhblFybGV3OEUrWERBWFUv - bTdXSnZFSU8vY1JJVTJjL3dLdWpiNGsKLS0tIHVlVmcrcTNZUGs4MmJtWkR0aTh3 - Z0pTOWhsZWYyVW5TeXk5ZHhaQnZvZ3MKrnBt5T7WjSxGYvRc1olfhuMN6nOEJbbX - xoliPcKkGsBExXVgkpood+OdlH8dNAaT0z1+INzNiBAZ8SazZA4p9g== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAvdUxUdzd2VmN1MWU2dXdM + N0FWcXkveVdHYk5aYzFrOVZaRzVYZWs1RFZBCkFwRy9xVFBrVjU0NmhkMlpyN0pp + ZlZKenhoNFVRdGg4Q0ZTMS9YUDFKMHcKLS0tIG1GYkZaYkhJNk5zdWZBMEtNL041 + MWhEQ1RmQ1k0d2Y2dzh2YnRvRm1IOEUKJ64/tNyLe2qBIL4CetRlpLaxhfOL93D5 + BjeHLwtfXB2fNCJSdCY8H4KsQP+epwriUQPtZVId32n9xDEt71obSw== -----END AGE ENCRYPTED FILE----- - recipient: age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0 enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjYk1SOVZxUTBWYUw4aUhy - WG93N2xVWDlEWnBqQ0JTRWcxc2tXUnppSFF3Cmp6L1g4OUliVlNFYUo2bEtNOHhm - aytHK0RFWmNpSWFMaGc1cnNXNlEwbTQKLS0tIHMxU3VXYTZHNWk3cWZFNXBNRElD - bE1CUHc1TFlsbGU2aTJjSmhwZENuUDgKvGHg2Df/eBw5akRPYFLvXhzh7P6jTOgj - E56n29EJ+p4kTkC8yVBci01qpMioL+Wx2Rt+X+0LWrGBzu5fic2U5g== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBRNi9kMkt5Vk4rS0pqY2ZP + ODErMlRxNmlWaHBvYjNaQ3lRT1RSQitMVlFBCk5HQ2o0QUg4dWFodTNEVGJFTlF0 + Y2JIVEFPZGVUdXpNS2lGOVQzeFRvMFEKLS0tIGtQY0dicE91R0J3eEM3bEdMZHJ4 + TndiYVFQWGZyRkF1bWYrRlZ6N0JBRm8KEUKY3chev13KjnGKdTR8tvyYV3s0W1rI + 8LeJSIocSX58PexqgcKsT8pGpuIiOetEzjzv2WPa0MzEwtFCVgwKYQ== -----END AGE ENCRYPTED FILE----- - recipient: age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6 enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAwSGdBYzlsYmc4dVJ5ZE8r - Q1FMR1NQL2lzZ1lmaTg1bGROa0hOcVk2ZTJvCllFMUtJdG5JVFhseWc5eXFsYk9p - Q2tIVm5yelAreVRrVkQrTnJ1N3FLclEKLS0tIGQ5dTdqZ3lKWU1KcHdHK1UwajIw - Znh5aXkwNmdyVi9EZlBzMUpTTW1VdjgKVVNGjiiKw3nLxq4YsCWoTS4R8wUD8wqD - awXSIOvZj5xiz6NvFk1X5T3H4XeEm6tKKzOpBvoVn5yl3zgn/bh5pA== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBFcktzeGx5VWZaeFVNc2Mv + aWt1dzNUVko5YytOTTdxMWVEVm85N29TMDBrClc3ODdVVDRVRFI3K3BiUGd1TFp1 + SE5nalY0RkwzNmZQOWU1THhGQ3Q0UkUKLS0tIHNZdHhxdDY0a1AyY0g3WUx3bUJh + U0RqRWRFRXNLTWFwemNOYk8vTmxZeTAKaZI6WSNM022xtZQx5yOYXj5mesAjWsu/ + ZIDG6MayoyrhwAkuZsWOKtKp6zbRCcnm7s9OHdlNsx7PWG1ODEFpOQ== + -----END AGE ENCRYPTED FILE----- + - recipient: age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBVbFFxSkhscUV2bm93Z01O + WDl3MU11K2lrNzAwUVN5MDhodDlveFZ6QlYwCjNaNjd3bG9QMTQxcVA5TTNQU3Za + bWdZTDdGZzJObnJ6NlBxKzVYcmpVNk0KLS0tIEJTcFVTRmkxQlJWSzVIM0ErOVNN + c290WkI1ZzBlYkg3QzJJUlQwOFZPYmMKQFp4vqFwh0GpYOur4kGMCeENnuNZnN0x + sbNmM9rRm/10T4tHF9aR2/WIjEqm6M6+yXd3phhB8rIggDPlVk2bCg== -----END AGE ENCRYPTED FILE----- lastmodified: "2026-05-21T20:13:20Z" mac: ENC[AES256_GCM,data:bi4o17skIOtZoxJGzLFJ7IAv+X265qIhgnQz6wr4bch2xofwPZZfzECck0I0xDhcwxcDtx3VZ5E2VBdvHWk+iSan+clRq7735k9+DIpjdWaGxIHuylmelZl6aE5kE36UO7fQdnwmiqCPHxiVq/sKuqknFXkl1+FvO1ryrtSHQsU=,iv:xxm+5rK7o2c43iGS1j2Q274QCRsbDn+k+wskpp5tzeU=,tag:sZHk9eth/TwqM8tIWSrv3w==,type:str] diff --git a/secrets/openrouter-api-key.yaml b/secrets/openrouter-api-key.yaml index d592db4..56a95e5 100644 --- a/secrets/openrouter-api-key.yaml +++ b/secrets/openrouter-api-key.yaml @@ -8,20 +8,29 @@ sops: - recipient: age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6 enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBUZEU0QWtxb0RMMkNqTHI3 - d25ONHF2Z2F6L0c4VWd6amMvYkQ0NU5YeEh3ClpBSEt5bXNJVmF5Q2pzNGdzVWVC - MzFDSTk2YlpyWkxIdDg0RmFxR21EMzgKLS0tIE1GRW52Wk10eXppM05XSDMrc3RH - eWQ5TFR0R29RWUxsbVhlWm1TVkFvb0UKwv6OafGdw06J21tQoUlvNI0aLVvl2CVm - nwbltuhQvWdFPVBRP/Cx0eK7Vh80tJrnOfXeGpNVgiCs2lpxBlUR2g== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3djYvT3JJUWh3cE1ZbFc0 + SiswOGlKNEdFNlJhTVlVSkhqZGlJdGl4U2pvCnV0aEhud0VaOHVGbkVVVkJ3b2Js + K1liaEFscGJxMXVVNjNJNW9NeGR6Um8KLS0tIEt2TlFCV09CWDJKbmFySm1EUXc3 + Zm9IT21QN0lkSlh0UVdhYkVrTDdiUWcK8IO+gylIYKDlSADOtj2gSpM7Af/JG/7Q + 6y1dT2d82cEu7KuXXuog6gP3sADe/6SDHGgC6Ot6EWM+5dwMN6WwTA== -----END AGE ENCRYPTED FILE----- - recipient: age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6 enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBMZVdubENvWkxGWmNnVks0 - NFJYQVBKMnJJZ3FEUDVJWXl2WitMTDFYdEd3CmJQaSt4bTZyUVlBZHgybXRhbWky - UGIzNkwzUzNZaWN3WmNKSUkxQkl0OTAKLS0tIENyNlphYmVRV3BUNU1FQzd0QStN - V0JLNU5RT09RVjd2OTJUTWMvek9NaTQK0R0OfQnm7AhIeroefldUNDsgDHX1cDmk - GybdGEoM8j8Is8GLc6Gk+yQmz7yNvVfYBkaaFsH3Crz3HupuyXI59g== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKY2xVSUVBUTd0dVhxekhY + d2t1U00xNnRjTC9PT3puVGo1U2ViekhmQng4CnJlZWYxK0tyQ2U0Y3lXRUZSTEti + cU9QdWhjalJhb3dJSk4vK1hWZGFGNEkKLS0tIG51aFRGcW9xdmdmSkt0TTZXSll4 + dkJMYjdPbUgyL1Rmc0tQbHgyR3RLZTAKllFoX3m/zntAtfGkSGFmRzXuk4pHALkR + XeQaAl33n5dMiZHtynoDNN3eBXtDDWiKZAhZeWI5SMoLpWBEXapNag== + -----END AGE ENCRYPTED FILE----- + - recipient: age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsdUpxd0RYRld3L1BYdllp + R3RwVStLUysyR1lXQ0RlcW4vWGRINzB3WUd3CktTSXJzUWsxcEdVajVjS3N1d0NE + WHU4QkVvU3dyYWZ0VVpmYnB2blVWaGcKLS0tIG5sa1I2YW1tS0hLeGc1SWtjV0NM + TkFoL0ZZbmhXdElFNytLclByK0s1LzgKVrBvlaryZXYn43v1ukjGZnhSPuwfslf7 + Ruy7z7EzVaCXRn+F9gABD4mhIQaUIRswiut7aF9lrPOR72/y8s/1JA== -----END AGE ENCRYPTED FILE----- lastmodified: "2026-05-30T23:08:58Z" mac: ENC[AES256_GCM,data:68i7qPK7zJXDspmVYfTVJGd141ldXeiPEHK/tsX3p9QHBSP8otXwAzaEqcjNG83dXhrse9CRzzyJmrLm7EO714hZyHKh6MKZb/BOuiwnMCZse0qbK55ghkzTQLMkeSuIM2KsN396mlH/Ehd/hQV6oC6OPq2RQUs/aJt55ESutLc=,iv:kGPKf9e6w1vz7soEyDL7mGQCBkrxiG0Wvm7FHEYp3jc=,tag:zHd7qoIx9Fei2Aabgsztuw==,type:str] diff --git a/secrets/turn-shared-secret.yaml b/secrets/turn-shared-secret.yaml index 3278bb4..bb15c65 100644 --- a/secrets/turn-shared-secret.yaml +++ b/secrets/turn-shared-secret.yaml @@ -8,29 +8,38 @@ sops: - recipient: age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6 enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBiaFhuUlZJMkxERlR1T2xL - dnVKMXNMNVBIdDFuYzJtTjJWZE9JZnlaQ1dJCnNNbzJKc3VXOUZ1QUFzRWhSUkdF - WVJlNUtJSytSS2h5NGQyMHA4d2U0c28KLS0tIFVhSGJQOWlEQlp6SlRmS0ZMZzJ4 - Y2ZoVVVkeVNiYUNwWFFLd2VGaTR1VUkK9Dpk0kjuKoUh3zfVQV7qs/YTTg2BaOkg - kNYY01k9MftEwdtvpKk1ogzdHyhGFJ1yEepwK7se6W5KHDJXyVfA8w== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBGbTFyeCtJVWNOUXlSNXQ0 + MEF6aG84NEVNUzlrMnoxcEF0ZFpEVC9LK1ZvCk04TkNkVWtVMWhhL2QwVEVEMi9X + M3Jkd1NoVFBFaDczMGhtN0svR284UzgKLS0tIE5OdTlXWXptTS9TZWdUNzFKU1VW + QXl2N2I3dFFORlFEbUVSK2ppSmNHSk0KzeS7uJCvQLd97XI/MfKuhJi4pQ/jKx2d + 5veiGfnmI/j/WCBdH35PMTEotbBBQ26uFt+uvcShK2gO62MVbht8PQ== -----END AGE ENCRYPTED FILE----- - recipient: age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0 enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4N2xmZEM3R2tMQjA4a1Fr - cWZ5a2lwUWlJZDdVSExnaHA3cEpubTVMWG1BCjNjTEJzL01PWm9xRWlEWUlWend5 - TUdMMjUzNE1RN0h5bzJuQnl5QkJXVzAKLS0tIDZ6aVh6dXlxTTVPZDRKQUkyZmlk - a2FLZ2s1NTA1ZUljeFBhSDZEcGZLNlkKcX5b3dXcJZejeSP4TLr1cOXQj7YEjj0G - znXBdDt6c42qE5XS+LGciunf3MmYxt0xXDb11cnSSEVgn0VyqE2x6w== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjWG9PME1XQ1dSNkVEME9N + ZlBNL0FnclpoeEVLTVk5emlnTmpFVWtLcVF3CmQrY1ExOG5wMmRoMkV4Y3hXSjU5 + OTZidFVWcklmZ0dHU0pJeTdiUTM5RzQKLS0tIHBGbncxRkpnUTVlNitRZkhyRjBE + bjdVSkZRZ241OE9Xd240OExWVkFrczAK+loPUAs5aBMC6XRY+yu0r3bqnWozldP6 + m6mnRhUl2+JGm5RHglDWibUwPZ9I1EVlufDwDCABBnoXnY/HWuOlAw== -----END AGE ENCRYPTED FILE----- - recipient: age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6 enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBybWtTZEw3NTdwMWZhOUMv - L1RNWGduU1BSM0FodWMwUEtHNkxSY3MvdGhzCm1qTGlnM2ZRM2RoekhZbjFzZ2tE - Qk1DNjlnMk01R1ZzUDBubTMvR2hPNmcKLS0tIGsxZ2lQMi9vY0Zpb3pwRHBSSm5t - OWtkeCtNNnpTVXV6S1p2Y2E0V2toL1EK3RI0RgM0SudRguOpOimke7niuX3cIIVi - X+zK75QbvLN7pe1pIIC2UOEmRB9BhgGMVssHxnLzTJv0LUKjDMGdbw== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAxOG96YzNTQk1sQitQNnV3 + V3FsUEN5b00wcys5TmJSY0lTTm1qaEV1MGtZCnlYa3ZMNTFTdkliM0NjVW9BLzhj + ZytCMDQrcjVmdEx0cjhNRllZY2xuWDAKLS0tIHhoMVR0KzhZb1dVMldIUjUyc2JS + OHBSbVliajNSTDR5VW9odkdpN01iT0UKzifxPVQc98lEB4sXoDcDw7t3R396iwa7 + RENR54u5GYUHHySq14v4k+7zhV42+xaXW45ZPhcA4BKuaAw8ygSDGQ== + -----END AGE ENCRYPTED FILE----- + - recipient: age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5R1EvVlZOOVMxSSsya2Fn + Zm5iWWN6d2RFNzU5aXBNbHNkcnYwM1VLY1Z3CjlEazNpcnhQdTRFMTVxdWliV0Ju + eEZMMnliS3BnU0RMSy9lNDZhVUw4K28KLS0tIG4vN01xZjRNSmVMVWx0VEJhd29Q + QTR3cEhvTVowU2ZXNDY3ZjhSVktSdEEKTxL2M/OgOMcptliInoED6aRDEQaqGnXF + N7dg2UnQsuFplJIfsW8KRoDQkcuXbdjn/W5aQh76+OC8wuRPVNWcEA== -----END AGE ENCRYPTED FILE----- lastmodified: "2026-05-21T20:13:20Z" mac: ENC[AES256_GCM,data:PXXEpDbJIoIwWuNwMetpALtxrcB7yJhDYqp6LSKV8WFQI2TJSDgDHiNQ2cngfB9PXKmUNNhlvhmB52Hi6WQSWQdXjiy79T1fOjMhbUAKmykTaZDrwvlrDGkPgiZ1cFSb8+hx/5aZw7YhdQO9+7LAmymeIldZIFEpEO2pvuPZ6MM=,iv:mJb3K8TnsnY183OX7o9pF7oVMDcwVU/rOgX74KTRpr4=,tag:f+M+FT0Vjozm7a8LFDI8lA==,type:str] From 5f929ce3ee54fd7226bbb7632da06a4709e4f3d9 Mon Sep 17 00:00:00 2001 From: root <root@hubris.local> Date: Mon, 1 Jun 2026 23:09:41 +0200 Subject: [PATCH 04/25] openrouter-api-key: seed real key --- secrets/openrouter-api-key.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/secrets/openrouter-api-key.yaml b/secrets/openrouter-api-key.yaml index 56a95e5..c13c6c3 100644 --- a/secrets/openrouter-api-key.yaml +++ b/secrets/openrouter-api-key.yaml @@ -1,4 +1,4 @@ -api_key: ENC[AES256_GCM,data:ps6aTT939SWpgyUoMMIj6YsJG3UnJFBdRKuESpoMeyphB9QnrkqSb5Y/kXI=,iv:6KxZTpPUR2GmPzr5opMP4Wp/yPIQqE9AWtAO5xBDGTI=,tag:qfRt1AlQ97PeHMsGmXnqsQ==,type:str] +api_key: ENC[AES256_GCM,data:ApbGI2FsuIxMNnZHXK1ikXiq448OGlHjHieltSyY1sSL2kjvUHc8D297scUHLiv10MRPE9QGqKw48UavkfPKlwRRoCF3Plt2uw==,iv:rZtFm7RaBJz0wRthmR5FS5nGc/dYk8XyTzPihNsQJEk=,tag:X1I58H2L5rRvx/XhpHwQmg==,type:str] sops: kms: [] gcp_kms: [] @@ -32,8 +32,8 @@ sops: TkFoL0ZZbmhXdElFNytLclByK0s1LzgKVrBvlaryZXYn43v1ukjGZnhSPuwfslf7 Ruy7z7EzVaCXRn+F9gABD4mhIQaUIRswiut7aF9lrPOR72/y8s/1JA== -----END AGE ENCRYPTED FILE----- - lastmodified: "2026-05-30T23:08:58Z" - mac: ENC[AES256_GCM,data:68i7qPK7zJXDspmVYfTVJGd141ldXeiPEHK/tsX3p9QHBSP8otXwAzaEqcjNG83dXhrse9CRzzyJmrLm7EO714hZyHKh6MKZb/BOuiwnMCZse0qbK55ghkzTQLMkeSuIM2KsN396mlH/Ehd/hQV6oC6OPq2RQUs/aJt55ESutLc=,iv:kGPKf9e6w1vz7soEyDL7mGQCBkrxiG0Wvm7FHEYp3jc=,tag:zHd7qoIx9Fei2Aabgsztuw==,type:str] + lastmodified: "2026-06-01T21:09:28Z" + mac: ENC[AES256_GCM,data:ypRPQlsr1pNpKTCADWtsKJ98Yqw2854fsBVN7EK2nLz7QtyDLO+sr0/Ts7J4yjOi12vYpF8zhhmHAsPl5XaaYmHXpcJHiu2CXkePt0aouyW5x6XwsO6ELNbCluiHgdETIhzyudJbQtdVxVznCijg+o7x4Wq8H/Jfj1VU2ol6fJo=,iv:d74mEyLPDQp1Ozqf7u5iOb/WuSvZ5flHwbss8MPuLzM=,tag:IsmMEi7uRnwOWdAH9+p9pQ==,type:str] pgp: [] unencrypted_suffix: _unencrypted version: 3.9.4 From 9be6f7f31df1d3c2221b6df0caf56a57fdea70d2 Mon Sep 17 00:00:00 2001 From: dtoro <d.toro.v@pm.me> Date: Mon, 1 Jun 2026 23:24:36 +0200 Subject: [PATCH 05/25] openrouter-api-key: seed real key --- secrets/openrouter-api-key.yaml | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/secrets/openrouter-api-key.yaml b/secrets/openrouter-api-key.yaml index c13c6c3..f37ffa1 100644 --- a/secrets/openrouter-api-key.yaml +++ b/secrets/openrouter-api-key.yaml @@ -1,12 +1,7 @@ -api_key: ENC[AES256_GCM,data:ApbGI2FsuIxMNnZHXK1ikXiq448OGlHjHieltSyY1sSL2kjvUHc8D297scUHLiv10MRPE9QGqKw48UavkfPKlwRRoCF3Plt2uw==,iv:rZtFm7RaBJz0wRthmR5FS5nGc/dYk8XyTzPihNsQJEk=,tag:X1I58H2L5rRvx/XhpHwQmg==,type:str] +api_key: ENC[AES256_GCM,data:TbPkuLCidS7cg52DYb0MOCui3TsaiwkOWyYLhp6gcvXI8TqvXej1tNsCF0TNAIr9vvnFaYno/UTEPiOKe/EQqR8uQMyWuvsmMg==,iv:bahy9ae4Qxvkv1OAjx6LwzK5ggqDh2GVHDj95PVP1Mc=,tag:J7PajLaeqFOdz3FESYZmSQ==,type:str] sops: - kms: [] - gcp_kms: [] - azure_kv: [] - hc_vault: [] age: - - recipient: age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6 - enc: | + - enc: | -----BEGIN AGE ENCRYPTED FILE----- YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3djYvT3JJUWh3cE1ZbFc0 SiswOGlKNEdFNlJhTVlVSkhqZGlJdGl4U2pvCnV0aEhud0VaOHVGbkVVVkJ3b2Js @@ -14,8 +9,8 @@ sops: Zm9IT21QN0lkSlh0UVdhYkVrTDdiUWcK8IO+gylIYKDlSADOtj2gSpM7Af/JG/7Q 6y1dT2d82cEu7KuXXuog6gP3sADe/6SDHGgC6Ot6EWM+5dwMN6WwTA== -----END AGE ENCRYPTED FILE----- - - recipient: age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6 - enc: | + recipient: age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6 + - enc: | -----BEGIN AGE ENCRYPTED FILE----- YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKY2xVSUVBUTd0dVhxekhY d2t1U00xNnRjTC9PT3puVGo1U2ViekhmQng4CnJlZWYxK0tyQ2U0Y3lXRUZSTEti @@ -23,8 +18,8 @@ sops: dkJMYjdPbUgyL1Rmc0tQbHgyR3RLZTAKllFoX3m/zntAtfGkSGFmRzXuk4pHALkR XeQaAl33n5dMiZHtynoDNN3eBXtDDWiKZAhZeWI5SMoLpWBEXapNag== -----END AGE ENCRYPTED FILE----- - - recipient: age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs - enc: | + recipient: age1vf8h7s8mqsn2q5eadgpdupsj4mwn8zguc77d85ws3xj40sl9rgksx2rxw6 + - enc: | -----BEGIN AGE ENCRYPTED FILE----- YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsdUpxd0RYRld3L1BYdllp R3RwVStLUysyR1lXQ0RlcW4vWGRINzB3WUd3CktTSXJzUWsxcEdVajVjS3N1d0NE @@ -32,8 +27,8 @@ sops: TkFoL0ZZbmhXdElFNytLclByK0s1LzgKVrBvlaryZXYn43v1ukjGZnhSPuwfslf7 Ruy7z7EzVaCXRn+F9gABD4mhIQaUIRswiut7aF9lrPOR72/y8s/1JA== -----END AGE ENCRYPTED FILE----- - lastmodified: "2026-06-01T21:09:28Z" - mac: ENC[AES256_GCM,data:ypRPQlsr1pNpKTCADWtsKJ98Yqw2854fsBVN7EK2nLz7QtyDLO+sr0/Ts7J4yjOi12vYpF8zhhmHAsPl5XaaYmHXpcJHiu2CXkePt0aouyW5x6XwsO6ELNbCluiHgdETIhzyudJbQtdVxVznCijg+o7x4Wq8H/Jfj1VU2ol6fJo=,iv:d74mEyLPDQp1Ozqf7u5iOb/WuSvZ5flHwbss8MPuLzM=,tag:IsmMEi7uRnwOWdAH9+p9pQ==,type:str] - pgp: [] + recipient: age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs + lastmodified: "2026-06-01T21:23:34Z" + mac: ENC[AES256_GCM,data:DXvUZUOMKQA0aDpN1foKy+aHhBh6daGSRJM1FagTh76qutk4XF9Een9iyy9sQ7olabCjf3oUezvU7XWB83bSDD3e23CFuweOl0RreRFqIsbXBi55Dbcxw8+9fvMyOK4PXu6mkT/PRao/XR5sSMqiHt3FoJbjazSaJlI5teeVXbM=,iv:IICO5ay+KqkTikfxW4kqB2qcSRpbsOP1NjGWtCB9uqI=,tag:MX2TqUxre+ff3RszCfXeFg==,type:str] unencrypted_suffix: _unencrypted version: 3.9.4 From 95a3ae75c31d0c64324b5a056338f012f8822011 Mon Sep 17 00:00:00 2001 From: dtoro <d.toro.v@pm.me> Date: Mon, 1 Jun 2026 23:28:43 +0200 Subject: [PATCH 06/25] hermes-agent: update model to deepseek/deepseek-v4-flash (tool-use support) --- operations/hermes-agent.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/operations/hermes-agent.md b/operations/hermes-agent.md index e39d56e..12b7cb7 100644 --- a/operations/hermes-agent.md +++ b/operations/hermes-agent.md @@ -115,7 +115,7 @@ The bootstrap-managed keys in `~/.config/goose/config.yaml`: ```yaml GOOSE_PROVIDER: openrouter -GOOSE_MODEL: nousresearch/hermes-4-405b +GOOSE_MODEL: deepseek/deepseek-v4-flash GOOSE_MODE: smart_approve # asks before destructive tool calls extensions: developer: From 946ded6d085be68e6e4813dfefafeae6e948e474 Mon Sep 17 00:00:00 2001 From: dtoro <d.toro.v@pm.me> Date: Mon, 1 Jun 2026 23:34:19 +0200 Subject: [PATCH 07/25] test credential --- test-credential | 1 + 1 file changed, 1 insertion(+) create mode 100644 test-credential diff --git a/test-credential b/test-credential new file mode 100644 index 0000000..1f52a9e --- /dev/null +++ b/test-credential @@ -0,0 +1 @@ +Mon Jun 1 23:34:19 CEST 2026 From 10fdb0fe09d394fc71ea29d8739d395513e22654 Mon Sep 17 00:00:00 2001 From: dtoro <d.toro.v@pm.me> Date: Mon, 1 Jun 2026 23:34:32 +0200 Subject: [PATCH 08/25] revert: test commit --- test-credential | 1 - 1 file changed, 1 deletion(-) delete mode 100644 test-credential diff --git a/test-credential b/test-credential deleted file mode 100644 index 1f52a9e..0000000 --- a/test-credential +++ /dev/null @@ -1 +0,0 @@ -Mon Jun 1 23:34:19 CEST 2026 From 19b2109efc8dd196c9442dc1478bec5c094730b7 Mon Sep 17 00:00:00 2001 From: dtoro <d.toro.v@pm.me> Date: Mon, 1 Jun 2026 23:41:06 +0200 Subject: [PATCH 09/25] test --- test-cred2 | 1 + 1 file changed, 1 insertion(+) create mode 100644 test-cred2 diff --git a/test-cred2 b/test-cred2 new file mode 100644 index 0000000..855a05c --- /dev/null +++ b/test-cred2 @@ -0,0 +1 @@ +Mon Jun 1 23:41:06 CEST 2026 From e615599a2c074f5279fce55bdbc2c4854ed8cea5 Mon Sep 17 00:00:00 2001 From: dtoro <d.toro.v@pm.me> Date: Mon, 1 Jun 2026 23:41:19 +0200 Subject: [PATCH 10/25] revert test --- test-cred2 | 1 - 1 file changed, 1 deletion(-) delete mode 100644 test-cred2 diff --git a/test-cred2 b/test-cred2 deleted file mode 100644 index 855a05c..0000000 --- a/test-cred2 +++ /dev/null @@ -1 +0,0 @@ -Mon Jun 1 23:41:06 CEST 2026 From daf4747c5c4171d730bc4d03cacc09b558e7c2ec Mon Sep 17 00:00:00 2001 From: dtoro <d.toro.v@pm.me> Date: Mon, 1 Jun 2026 23:45:22 +0200 Subject: [PATCH 11/25] =?UTF-8?q?post-mortem:=20mac-mini=20onboarding=20?= =?UTF-8?q?=E2=80=94=2012=20issues=20found?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2026-06-01-mac-mini-onboarding.md | 185 ++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 investigations/2026-06-01-mac-mini-onboarding.md diff --git a/investigations/2026-06-01-mac-mini-onboarding.md b/investigations/2026-06-01-mac-mini-onboarding.md new file mode 100644 index 0000000..99eb1ce --- /dev/null +++ b/investigations/2026-06-01-mac-mini-onboarding.md @@ -0,0 +1,185 @@ +# mac-mini onboarding — post-mortem & lessons learned + +Onboarded the `mac-mini` workstation (macOS Sequoia, arm64) into the hubris +homelab context system with the `--with-hermes` profile. What follows is a +chronological recap of every hitch, the fix, and the systemic improvements +needed so the next workstation takes 5 min instead of an hour. + +## Session log + +### Step 1 — clone + symlink +- Manually cloned `git.hubris.network/dtoro/Homelab-Docs` to `/Users/dtoro/Homelab-Docs`. +- Created `/opt/homelab-context` → `/Users/dtoro/Homelab-Docs` symlink. +- **Lesson:** bootstrap.sh was designed to do this from scratch, but we'd + already cloned by hand. The bootstrap's `clone exists; pulling` path handled + it gracefully. + +### Step 2 — hostname mismatch +- `scutil --get LocalHostName` → `Davids-Mac-mini` +- `hostname -s` → `Mac` +- Inventory file: `hosts/mac-mini.yaml` +- **Fix:** `sudo scutil --set LocalHostName mac-mini && sudo scutil --set HostName mac-mini` +- **Lesson:** The bootstrap and `homelab whoami` use different hostname + resolution. Bootstrap uses `scutil --get LocalHostName` (correct on macOS), + but the `homelab` CLI binary uses `hostname -s`. Both need to match the + inventory key. On a fresh macOS machine, neither does. + +### Step 3 — bootstrap dependencies +- pyyaml was missing → `pip install pyyaml` +- age and sops were missing → `brew install age sops` +- Netbird was already installed and connected ✓ +- **Lesson:** The bootstrap preflight handles these, but only if running + `bootstrap.sh` from the start. Since we ran it after manual setup, some + steps (netbird install) were correctly skipped as already-present. + +### Step 4 — full bootstrap with `--with-mcp --with-hermes` +- Ran `sudo HOMELAB_GITEA_TOKEN=... bash bootstrap.sh --with-mcp --with-hermes` +- Age key issued ✓ +- Launchd sync timer installed ✓ +- Goose binary installed ✓ +- Hermes CLI linked ✓ +- MCP config merged ✓ +- `refresh-creds` skipped (not yet a recipient) ⚠️ +- Cosmetics: `chown: dtoro: illegal group name` at the end (benign, macOS + group-naming quirk) + +### Step 5 — finalize from hubris +- Ran `homelab client add mac-mini --finalize-pubkey <age...> --with-hermes` on + hubris +- Push failed: `[rejected] main -> main (fetch first)` — hubris clone was + stale, bootstrap had already pushed from mac-mini +- **Fix:** `git pull --rebase && git push` on hubris +- **Lesson:** bootstrap pushes remote changes before hubris can finalize, + creating a race. The `homelab client add --finalize-pubkey` command should + pull before committing/pushing. + +### Step 6 — sops couldn't find the age key +- `homelab secret hello` failed because sops looks in + `/Users/dtoro/.ssh/id_rsa` etc. by default, not `/etc/age/key.txt` +- The `homelab` CLI re-execs via `sudo -E env SOPS_AGE_KEY_FILE=... sops ...`, + but this requires passwordless sudo and the correct env var passthrough +- **Fix:** + 1. Added NOPASSWD sudo rules + 2. Eventually `SOPS_AGE_KEY` env with the raw key content worked directly +- **Lesson:** Document the explicit `SOPS_AGE_KEY_FILE` incantation in + agent-enrollment troubleshooting. New clients can't assume `homelab secret` + works out of the gate — the sudo re-exec chain has permission pitfall. + +### Step 7 — OpenRouter key was a placeholder +- `secrets/openrouter-api-key.yaml` contained + `api_key: PLACEHOLDER_REPLACE_WITH_REAL_OPENROUTER_KEY` +- User ran `sops` on hubris, but got the same error (age key not found on + hubris either — `/root/.config/sops/age/keys.txt` didn't exist) +- **Fix:** `SOPS_AGE_KEY_FILE=/etc/age/key.txt sops ...` on hubris. + Later: the user pasted the real key, but the sops file showed + `sk-or-...5c55` — the literal content was truncated with ellipsis. + +### Step 8 — editor loaded the wrong data +- Neovim on the system is configured with `clipboard+=unnamedplus`, which + points `*` and `+` registers to the macOS clipboard manager rather than + X11. When editing SOPS files, this caused the **system clipboard** to be + pasted instead of the actual ciphertext. +- This wasn't diagnosed during the session — the sops file would load empty + or show the wrong content because the editor's idea of "paste" was + disconnected from what sops expected. +- **Fix:** Run `sops` with `EDITOR=nano` or another editor that doesn't + hijack OS clipboards: + ```bash + EDITOR=nano SOPS_AGE_KEY_FILE=/etc/age/key.txt sops secrets/openrouter-api-key.yaml + ``` +- **Lesson:** Add a strong warning to `hermes-agent.md` / `agent-enrollment.md`: + macOS neovim with `clipboard+=unnamedplus` silently breaks sops editing + because the paste register reads from the system clipboard instead of the + sops-managed buffer. Use `EDITOR=nano` or `EDITOR=vim` when running sops + interactively. Alternatively, override the clipboard option with + `EDITOR='nvim -c "set clipboard="'`. +- Also useful for the troubleshooting table in `agent-enrollment.md` under a + new row: "sops file loads empty / wrong content on macOS" + +### Step 9 — model doesn't support tool use +- Goose config pinned `nousresearch/hermes-4-405b` via OpenRouter +- Error: `No endpoints found that support tool use` +- **Fix:** Switched to `deepseek/deepseek-v4-flash` in + `~/.config/goose/config.yaml` +- Also updated `operations/hermes-agent.md` with the correct model +- **Lesson:** The default model in `bootstrap.sh` and `hermes-agent.md` was + never validated on OpenRouter for tool-use capability. Need to either: + (a) Pin a model known to work (`deepseek/deepseek-v4-flash`) + (b) Or have the bootstrap probe OpenRouter at install time + +### Step 10 — MCP homelab extension returns 404 +- Goose's homelab extension configured as `streamable_http` at + `https://mcp.hubris.network/mcp` returns HTTP 404 +- The actual MCP server runs on SSE (`/sse`), not streamable_http +- **Known follow-up** from `hermes-agent.md` — server needs migration +- **Workaround:** The agent works fine via the built-in `developer` extension + (shell + file) and the `homelab` CLI + +### Step 11 — git credential dance +Several layers of issues with git authentication: + +1. **Directory permissions:** `/etc/homelab-context` was `0700 root` — dtoro + couldn't access the credential file, and git's `store` helper couldn't + create its `.lock` file. + - **Fix 1a:** `sudo chmod 755 /etc/homelab-context` + - **Fix 1b:** `sudo chown -R dtoro:staff /etc/homelab-context` + +2. **macOS built-in osxkeychain:** Apple's git has `osxkeychain` compiled in + as a default credential helper. Even after configuring the `store` helper, + git calls osxkeychain after every successful `get`, which prompts for + macOS keychain access. + - `credential.helper = ""` does NOT disable the compiled-in default + - The `store` helper's `.lock` file in `/etc/homelab-context/` also failed + silently, causing fallback behaviour + - **Fix:** Embed the credential in the remote URL directly: + `https://dtoro:TOKEN@git.hubris.network/dtoro/Homelab-Docs.git` + - This bypasses all credential helpers entirely + +3. **refresh-creds:** The credential is now managed by the remote URL. + If the PAT is rotated, `homelab refresh-creds` won't update the URL. + - **Workaround:** Run `git remote set-url origin` after `refresh-creds` + +### Step 12 — `homelab` CLI hostname detection +- `homelab whoami` failed with `no hosts/Mac.yaml` because the CLI uses + `hostname -s` (returning `Mac`) while the bootstrap uses + `scutil --get LocalHostName` (which we fixed to `mac-mini`) +- **Fix:** Set both `LocalHostName` and `HostName` via `scutil` +- **Lesson:** The homelab CLI on macOS should prefer `scutil --get + LocalHostName` like the bootstrap does, or at least try both and pick the + one that matches an inventory key. This is a CLI bug. + +## Improvement backlog + +### High priority + +| # | Issue | Fix | +|---|-------|-----| +| 1 | Homelab CLI uses `hostname -s` on macOS; should use `scutil --get LocalHostName` to match bootstrap | Patch `bin/homelab` to try `scutil` first, fall back to `hostname -s` | +| 2 | `homelab client add --finalize-pubkey` on hubris fails to push when remote is ahead (bootstrap pushes first) | `pull --rebase` before committing in `cmd_client_add` | +| 3 | Default Goose model (`nousresearch/hermes-4-405b`) doesn't support tool use on OpenRouter | Update `bootstrap.sh` default and `hermes-agent.md` to `deepseek/deepseek-v4-flash` | +| 4 | MCP server needs streamable_http migration | Follow-up #1 from `hermes-agent.md` — FastMCP `transport="sse"` → `transport="streamable_http"` | +| 5 | `homelab refresh-creds` should also update embedded remote URLs | In `cmd_refresh_creds`, after writing the credential file, update any remote URL that has an embedded credential | + +### Medium priority + +| # | Issue | Fix | +|---|-------|-----| +| 6 | macOS neovim with `clipboard+=unnamedplus` breaks sops editing | Add troubleshooting row to `agent-enrollment.md`; recommend `EDITOR=nano` | +| 7 | `/etc/homelab-context` directory with 0700 root permissions blocks non-root git | Change to 755 in `bootstrap.sh` on all OSes (or at least macOS) | +| 8 | Bootstrap `chown` at end gives `illegal group name` on macOS | Fix group detection in bootstrap.sh for macOS | +| 9 | `homelab secret` re-exec via sudo chain is fragile — needs passwordless sudo or TTY | Consider `SOPS_AGE_KEY` env-var fallback path in the CLI itself | + +### Low priority + +| # | Issue | Fix | +|---|-------|-----| +| 10 | Onboarding this machine revealed no `HERMES.md` file in the repo | Create the persona file for the Goose hints symlink | +| 11 | Git credential `store` helper lock mechanism is OS-sensitive and fragile | Document embedded-URL pattern as the preferred approach for workstations | +| 12 | AGENTS.md references `/opt/homelab-context/hosts/<hostname>.yaml` but the canonical path is now a symlink | Verify the path resolution edge cases | + +## Changelog + +### 2026-06-01 — initial post-mortem +Captured the full onboarding of mac-mini into the hubris homelab context +system, including the hostname mismatch, sudo/sops credential chain, model +selection, git credential gymnastics, and editor clipboard interference. From 0a3e43473e8b5f27194903e6d0fa0121d8b891d7 Mon Sep 17 00:00:00 2001 From: dtoro <d.toro.v@pm.me> Date: Tue, 2 Jun 2026 00:00:47 +0200 Subject: [PATCH 12/25] inventory: add lan_ip for mac-mini (192.168.8.174) --- inventory.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/inventory.yaml b/inventory.yaml index 072eb98..b76f253 100644 --- a/inventory.yaml +++ b/inventory.yaml @@ -334,6 +334,7 @@ hosts: kind: workstation os: macos role: dev + lan_ip: 192.168.8.174 mesh: netbird: fqdn: mac-mini-234-17.netbird.selfhosted From 5adb66237e01b635115889e99b4a0386affb37e9 Mon Sep 17 00:00:00 2001 From: dtoro <d.toro.v@pm.me> Date: Tue, 2 Jun 2026 00:05:03 +0200 Subject: [PATCH 13/25] ssh: add authorized_keys dir + deploy-keys.sh for key distribution --- ssh/authorized_keys/mac-mini.pub | 1 + ssh/deploy-keys.sh | 84 ++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 ssh/authorized_keys/mac-mini.pub create mode 100644 ssh/deploy-keys.sh diff --git a/ssh/authorized_keys/mac-mini.pub b/ssh/authorized_keys/mac-mini.pub new file mode 100644 index 0000000..1eb1922 --- /dev/null +++ b/ssh/authorized_keys/mac-mini.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMtgjgpljZcu98NOW1ibry8ZBQSoUg0IaxK8iW10w2+ d.toro.v@pm.me diff --git a/ssh/deploy-keys.sh b/ssh/deploy-keys.sh new file mode 100644 index 0000000..4050944 --- /dev/null +++ b/ssh/deploy-keys.sh @@ -0,0 +1,84 @@ +#!/bin/bash +# deploy-keys.sh — deploy workstation SSH pubkeys to hubris + all LXCs +# +# Run from hubris (PVE host) as root. +# Reads keys from /opt/homelab-context/ssh/authorized_keys/*.pub +# and appends them to /root/.ssh/authorized_keys in each running LXC, +# and to /etc/pve/priv/authorized_keys on hubris. +# +# Idempotent — skips keys already present. + +set -euo pipefail + +KEYS_DIR="/opt/homelab-context/ssh/authorized_keys" + +if ! [ -d "$KEYS_DIR" ]; then + echo "ERROR: $KEYS_DIR not found. Is homelab context synced?" + exit 1 +fi + +# Collect all pubkeys into a single variable, one per line +ALL_KEYS="" +for f in "$KEYS_DIR"/*.pub; do + [ -f "$f" ] || continue + key=$(cat "$f" | head -1) + ALL_KEYS="${ALL_KEYS}${key} +" +done + +if [ -z "$ALL_KEYS" ]; then + echo "ERROR: no .pub files found in $KEYS_DIR" + exit 1 +fi + +echo "= Deploying SSH keys to hubris =" +AUTH_FILE="/etc/pve/priv/authorized_keys" +touch "$AUTH_FILE" +added=0 +while IFS= read -r key; do + [ -z "$key" ] && continue + if ! grep -qF "$key" "$AUTH_FILE" 2>/dev/null; then + echo " + Adding key to hubris: ${key:0:40}..." + echo "$key" >> "$AUTH_FILE" + added=$((added + 1)) + fi +done <<< "$ALL_KEYS" +echo " hubris: $added key(s) added" + +# Deploy to LXCs +echo "" +echo "= Deploying SSH keys to LXCs =" + +# pct list output: VMID Status Lock Name +pct list | tail -n +2 | while read -r vmid status _ name; do + if [ "$status" != "running" ]; then + echo " SKIP $name ($vmid): status=$status" + continue + fi + + echo " -> $name ($vmid)" + + # Ensure .ssh directory exists + pct exec "$vmid" -- mkdir -p /root/.ssh 2>/dev/null + + # For each key, check if already present, append if not + while IFS= read -r key; do + [ -z "$key" ] && continue + if ! pct exec "$vmid" -- grep -qF "$key" /root/.ssh/authorized_keys 2>/dev/null; then + echo " + ${key:0:40}..." + # Use tee to append (pct exec preserves stdin) + echo "$key" | pct exec "$vmid" -- tee -a /root/.ssh/authorized_keys >/dev/null 2>&1 + fi + done <<< "$ALL_KEYS" + + # Fix permissions + pct exec "$vmid" -- chmod 600 /root/.ssh/authorized_keys 2>/dev/null || true + pct exec "$vmid" -- chmod 700 /root/.ssh 2>/dev/null || true + + echo " <- $name done" +done + +echo "" +echo "=== Deploy complete ===" +echo "Keys deployed. Test from any workstation with:" +echo " ssh root@<lxc-ip>" \ No newline at end of file From 00a891b1549cc1e6d4913d0bdc40abcb1cbbc5c6 Mon Sep 17 00:00:00 2001 From: dtoro <d.toro.v@pm.me> Date: Tue, 2 Jun 2026 00:06:58 +0200 Subject: [PATCH 14/25] ssh: fix LXC name parsing in deploy-keys.sh (Lock column edge case) --- HERMES.md | 50 +++++++++++++++++++++++++++++++ bootstrap.sh | 4 +-- containers/105-apps.md | 2 +- hosts/apps.yaml | 4 +-- hosts/arriman.yaml | 2 +- hosts/authentik.yaml | 2 +- hosts/caddy.yaml | 2 +- hosts/claudio-bot.yaml | 2 +- hosts/elementsynapse.yaml | 2 +- hosts/gitea.yaml | 2 +- hosts/haos.yaml | 2 +- hosts/hubris.yaml | 2 +- hosts/jellyfin.yaml | 2 +- hosts/ludo-mini.yaml | 2 +- hosts/mac-mini.yaml | 3 +- hosts/mule-images.yaml | 2 +- hosts/netbird-vps.yaml | 2 +- hosts/nextcloud.yaml | 2 +- hosts/nfs-export.yaml | 2 +- hosts/paperless.yaml | 2 +- hosts/plato.yaml | 2 +- hosts/republic-laptop.yaml | 2 +- hosts/sophia.yaml | 2 +- hosts/zimaos.yaml | 2 +- infrastructure/homelab-context.md | 2 +- inventory.yaml | 2 +- mcp/server.py | 2 +- operations/agent-enrollment.md | 4 +-- ssh/deploy-keys.sh | 11 +++++-- 29 files changed, 90 insertions(+), 32 deletions(-) create mode 100644 HERMES.md diff --git a/HERMES.md b/HERMES.md new file mode 100644 index 0000000..3be27b0 --- /dev/null +++ b/HERMES.md @@ -0,0 +1,50 @@ +# HERMES.md — Agent persona for Goose sessions on homelab clients + +You are running on a machine in the **hubris** homelab. Your primary context +is `/opt/homelab-context/AGENTS.md` — read that first. + +## Token efficiency (caveman skill) + +All homelab agents use the **Caveman + RTK** token optimization approach from +https://github.com/adityahimaone/hermes-agent-rtk-caveman. + +### Before running any CLI command, ask: + +1. **Is there a caveman wrapper equivalent?** Use the wrapper for token-efficient + output. Available wrappers (installed at `~/bin/caveman_wrapper.sh`): + - `~/bin/caveman_wrapper.sh git-status` — compact git status + - `~/bin/caveman_wrapper.sh git-log [n]` — compact git log + - `~/bin/caveman_wrapper.sh lint [target]` — compact lint results + - `~/bin/caveman_wrapper.sh test-results [cmd]` — compact test results + +2. **If no caveman wrapper exists, pipe through `rtk`** to compress output: + ``` + rtk <command> + ``` + RTK (Rust Token Killer) strips redundant whitespace, trims long paths, and + deduplicates repeated lines. This reduces token usage by 60-90% on CLI + operations. + +3. **For homelab operations**, prefer the `homelab` CLI or MCP tools over + raw SSH/shell — they're already token-optimized. + +### Templates + +Caveman templates live at `~/templates/`: +- `git_status.txt` — compact status format +- `git_log.txt` — compact log format +- `lint_results.txt` — compact ESLint format +- `test_results.txt` — compact vitest/jest format + +### When to skip caveman/rtk + +- Interactive commands (editors, prompts) — let human-readable output pass +- Commands with no output — skip entirely +- When you need the exact raw output for post-processing + +## Verification + +```bash +# Check caveman is installed +ls ~/bin/caveman_wrapper.sh && echo "caveman ready" +``` diff --git a/bootstrap.sh b/bootstrap.sh index 239e503..090af9b 100755 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -23,8 +23,8 @@ REPO_HTTPS="${HOMELAB_REPO_URL:-https://git.hubris.network/dtoro/Homelab-Docs.gi CLONE_DIR="${HOMELAB_CONTEXT_DIR:-/opt/homelab-context}" ISSUANCE_URL_NETBIRD="${HOMELAB_ISSUANCE_NETBIRD:-https://secrets.hubris.network/issue}" ISSUANCE_URL_TAILSCALE="${HOMELAB_ISSUANCE_TAILSCALE:-https://secrets.hubris.network/issue}" -MCP_URL="${HOMELAB_MCP_URL:-https://mcp.hubris.network/sse}" -HERMES_MCP_URI="${HOMELAB_HERMES_MCP_URI:-https://mcp.hubris.network/mcp}" +MCP_URL="${HOMELAB_MCP_URL:-https://mcp.hubris.network}" +HERMES_MCP_URI="${HOMELAB_HERMES_MCP_URI:-https://mcp.hubris.network}" HERMES_MODEL="${HOMELAB_HERMES_MODEL:-nousresearch/hermes-4-405b}" WITH_MCP=0 diff --git a/containers/105-apps.md b/containers/105-apps.md index f7fc582..f277e61 100644 --- a/containers/105-apps.md +++ b/containers/105-apps.md @@ -60,7 +60,7 @@ is `dtoro/Homelab-Docs/mcp/server.py`; service unit disabled at the FastMCP layer because mesh+LAN gating is the actual trust boundary. -- Endpoint: `https://mcp.hubris.network/sse` (Caddy → `:9810`). +- Endpoint: `https://mcp.hubris.network` (Caddy → `:9810`). StreamableHTTP transport (POST `/`). - 14 tools registered: `get_host`, `list_services`, `find_service`, `get_topology`, `search_docs`, `get_page`, `get_changelog`, `whoami`, `list_my_secrets` (context); `get_service_status`, `tail_log`, diff --git a/hosts/apps.yaml b/hosts/apps.yaml index 4a47624..aa76910 100644 --- a/hosts/apps.yaml +++ b/hosts/apps.yaml @@ -38,7 +38,7 @@ services_hosted: port: 9810 systemd_unit: homelab-mcp public_host: mcp.hubris.network - endpoint: https://mcp.hubris.network/sse + endpoint: https://mcp.hubris.network 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). - name: secrets_issuance @@ -51,5 +51,5 @@ services_hosted: age_pubkey: age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0 see_also: - containers/105-apps.md -mcp_endpoint: https://mcp.hubris.network/sse +mcp_endpoint: https://mcp.hubris.network secrets_issuance_endpoint: https://secrets.hubris.network/issue diff --git a/hosts/arriman.yaml b/hosts/arriman.yaml index 8f5df9a..a8dd7d6 100644 --- a/hosts/arriman.yaml +++ b/hosts/arriman.yaml @@ -30,5 +30,5 @@ services_hosted: note: jellyseerr / qbit / sab on docker compose see_also: - containers/122-arriman.md -mcp_endpoint: https://mcp.hubris.network/sse +mcp_endpoint: https://mcp.hubris.network secrets_issuance_endpoint: https://secrets.hubris.network/issue diff --git a/hosts/authentik.yaml b/hosts/authentik.yaml index 9329b85..8e98999 100644 --- a/hosts/authentik.yaml +++ b/hosts/authentik.yaml @@ -28,5 +28,5 @@ notes: - 'Also hosts split-horizon dnsmasq: /etc/dnsmasq.d/hubris-split.conf' see_also: - containers/124-authentik.md -mcp_endpoint: https://mcp.hubris.network/sse +mcp_endpoint: https://mcp.hubris.network secrets_issuance_endpoint: https://secrets.hubris.network/issue diff --git a/hosts/caddy.yaml b/hosts/caddy.yaml index 76780e8..12537f7 100644 --- a/hosts/caddy.yaml +++ b/hosts/caddy.yaml @@ -28,5 +28,5 @@ notes: - /etc/caddy is a git checkout of dtoro/caddy-conf see_also: - containers/121-caddy.md -mcp_endpoint: https://mcp.hubris.network/sse +mcp_endpoint: https://mcp.hubris.network secrets_issuance_endpoint: https://secrets.hubris.network/issue diff --git a/hosts/claudio-bot.yaml b/hosts/claudio-bot.yaml index e463c33..a0fb3bd 100644 --- a/hosts/claudio-bot.yaml +++ b/hosts/claudio-bot.yaml @@ -18,5 +18,5 @@ notes: age_pubkey: age1xmkeq968areza2necqyq0065dpeegngzyr6dhagh0n6pl33lccfqe5mqn9 see_also: - containers/123-claudio-bot.md -mcp_endpoint: https://mcp.hubris.network/sse +mcp_endpoint: https://mcp.hubris.network secrets_issuance_endpoint: https://secrets.hubris.network/issue diff --git a/hosts/elementsynapse.yaml b/hosts/elementsynapse.yaml index 32838b4..c2b4f51 100644 --- a/hosts/elementsynapse.yaml +++ b/hosts/elementsynapse.yaml @@ -25,5 +25,5 @@ services_hosted: backend: elementsynapse see_also: - containers/118-elementsynapse.md -mcp_endpoint: https://mcp.hubris.network/sse +mcp_endpoint: https://mcp.hubris.network secrets_issuance_endpoint: https://secrets.hubris.network/issue diff --git a/hosts/gitea.yaml b/hosts/gitea.yaml index 49ee4bf..dde53fb 100644 --- a/hosts/gitea.yaml +++ b/hosts/gitea.yaml @@ -30,5 +30,5 @@ notes: - Bare repos live at /mnt/library/repos/dtoro/*.git see_also: - containers/104-gitea.md -mcp_endpoint: https://mcp.hubris.network/sse +mcp_endpoint: https://mcp.hubris.network secrets_issuance_endpoint: https://secrets.hubris.network/issue diff --git a/hosts/haos.yaml b/hosts/haos.yaml index 9490069..08227bd 100644 --- a/hosts/haos.yaml +++ b/hosts/haos.yaml @@ -23,5 +23,5 @@ services_hosted: backend: haos see_also: - vms/108-haos.md -mcp_endpoint: https://mcp.hubris.network/sse +mcp_endpoint: https://mcp.hubris.network secrets_issuance_endpoint: https://secrets.hubris.network/issue diff --git a/hosts/hubris.yaml b/hosts/hubris.yaml index 89aff22..877d61e 100644 --- a/hosts/hubris.yaml +++ b/hosts/hubris.yaml @@ -31,5 +31,5 @@ services_hosted: age_pubkey: age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6 see_also: - hosts/hubris.md -mcp_endpoint: https://mcp.hubris.network/sse +mcp_endpoint: https://mcp.hubris.network secrets_issuance_endpoint: https://secrets.hubris.network/issue diff --git a/hosts/jellyfin.yaml b/hosts/jellyfin.yaml index f77a5e1..b50c70d 100644 --- a/hosts/jellyfin.yaml +++ b/hosts/jellyfin.yaml @@ -27,5 +27,5 @@ services_hosted: backend: jellyfin see_also: - containers/101-jellyfin.md -mcp_endpoint: https://mcp.hubris.network/sse +mcp_endpoint: https://mcp.hubris.network secrets_issuance_endpoint: https://secrets.hubris.network/issue diff --git a/hosts/ludo-mini.yaml b/hosts/ludo-mini.yaml index 341d59c..e3f651b 100644 --- a/hosts/ludo-mini.yaml +++ b/hosts/ludo-mini.yaml @@ -13,5 +13,5 @@ mesh_globals: accepted: - netbird - tailscale -mcp_endpoint: https://mcp.hubris.network/sse +mcp_endpoint: https://mcp.hubris.network secrets_issuance_endpoint: https://secrets.hubris.network/issue diff --git a/hosts/mac-mini.yaml b/hosts/mac-mini.yaml index a608ac3..416df8b 100644 --- a/hosts/mac-mini.yaml +++ b/hosts/mac-mini.yaml @@ -5,6 +5,7 @@ name: mac-mini kind: workstation os: macos role: dev +lan_ip: 192.168.8.174 mesh: netbird: fqdn: mac-mini-234-17.netbird.selfhosted @@ -18,5 +19,5 @@ ssh: notes: - Only macOS in the fleet. Bootstrap uses launchd. age_pubkey: age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs -mcp_endpoint: https://mcp.hubris.network/sse +mcp_endpoint: https://mcp.hubris.network secrets_issuance_endpoint: https://secrets.hubris.network/issue diff --git a/hosts/mule-images.yaml b/hosts/mule-images.yaml index d402715..fd3f4dc 100644 --- a/hosts/mule-images.yaml +++ b/hosts/mule-images.yaml @@ -27,5 +27,5 @@ services_hosted: backend: mule-images see_also: - containers/120-mule-images.md -mcp_endpoint: https://mcp.hubris.network/sse +mcp_endpoint: https://mcp.hubris.network secrets_issuance_endpoint: https://secrets.hubris.network/issue diff --git a/hosts/netbird-vps.yaml b/hosts/netbird-vps.yaml index 8cc3fee..13d026c 100644 --- a/hosts/netbird-vps.yaml +++ b/hosts/netbird-vps.yaml @@ -25,5 +25,5 @@ notes: - 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. -mcp_endpoint: https://mcp.hubris.network/sse +mcp_endpoint: https://mcp.hubris.network secrets_issuance_endpoint: https://secrets.hubris.network/issue diff --git a/hosts/nextcloud.yaml b/hosts/nextcloud.yaml index d8782cb..16ff6c3 100644 --- a/hosts/nextcloud.yaml +++ b/hosts/nextcloud.yaml @@ -27,5 +27,5 @@ services_hosted: backend: nextcloud see_also: - containers/114-nextcloud.md -mcp_endpoint: https://mcp.hubris.network/sse +mcp_endpoint: https://mcp.hubris.network secrets_issuance_endpoint: https://secrets.hubris.network/issue diff --git a/hosts/nfs-export.yaml b/hosts/nfs-export.yaml index 8381943..28ebe8c 100644 --- a/hosts/nfs-export.yaml +++ b/hosts/nfs-export.yaml @@ -15,5 +15,5 @@ mesh_globals: - tailscale see_also: - containers/102-nfs-export.md -mcp_endpoint: https://mcp.hubris.network/sse +mcp_endpoint: https://mcp.hubris.network secrets_issuance_endpoint: https://secrets.hubris.network/issue diff --git a/hosts/paperless.yaml b/hosts/paperless.yaml index 43a8fa4..1fb426d 100644 --- a/hosts/paperless.yaml +++ b/hosts/paperless.yaml @@ -27,5 +27,5 @@ services_hosted: backend: paperless see_also: - containers/103-paperless.md -mcp_endpoint: https://mcp.hubris.network/sse +mcp_endpoint: https://mcp.hubris.network secrets_issuance_endpoint: https://secrets.hubris.network/issue diff --git a/hosts/plato.yaml b/hosts/plato.yaml index 318da3a..dc7169d 100644 --- a/hosts/plato.yaml +++ b/hosts/plato.yaml @@ -24,5 +24,5 @@ services_hosted: backend: plato see_also: - containers/126-plato.md -mcp_endpoint: https://mcp.hubris.network/sse +mcp_endpoint: https://mcp.hubris.network secrets_issuance_endpoint: https://secrets.hubris.network/issue diff --git a/hosts/republic-laptop.yaml b/hosts/republic-laptop.yaml index 1962ddd..f975a78 100644 --- a/hosts/republic-laptop.yaml +++ b/hosts/republic-laptop.yaml @@ -15,5 +15,5 @@ mesh_globals: - tailscale ssh: user: dtoro -mcp_endpoint: https://mcp.hubris.network/sse +mcp_endpoint: https://mcp.hubris.network secrets_issuance_endpoint: https://secrets.hubris.network/issue diff --git a/hosts/sophia.yaml b/hosts/sophia.yaml index 62d08c0..64774aa 100644 --- a/hosts/sophia.yaml +++ b/hosts/sophia.yaml @@ -20,5 +20,5 @@ mounts: - /mnt/library see_also: - containers/119-sophia.md -mcp_endpoint: https://mcp.hubris.network/sse +mcp_endpoint: https://mcp.hubris.network secrets_issuance_endpoint: https://secrets.hubris.network/issue diff --git a/hosts/zimaos.yaml b/hosts/zimaos.yaml index ff105a7..7020217 100644 --- a/hosts/zimaos.yaml +++ b/hosts/zimaos.yaml @@ -22,5 +22,5 @@ services_hosted: backend: zimaos see_also: - vms/100-zimaos.md -mcp_endpoint: https://mcp.hubris.network/sse +mcp_endpoint: https://mcp.hubris.network secrets_issuance_endpoint: https://secrets.hubris.network/issue diff --git a/infrastructure/homelab-context.md b/infrastructure/homelab-context.md index 8dc40ec..b839b03 100644 --- a/infrastructure/homelab-context.md +++ b/infrastructure/homelab-context.md @@ -16,7 +16,7 @@ page is the architecture reference. | Per-client clone | every enrolled client | `/opt/homelab-context/` | Read by `homelab` CLI, MCP server, claudio-bot | | `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)](../containers/105-apps.md) | `homelab-mcp.service` on port 9810 (https://mcp.hubris.network/sse) | 14 tools: 8 context (get_host, search_docs, …) + 5 read-only management (get_service_status, tail_log, …) + list_my_secrets | +| MCP server | [apps (105)](../containers/105-apps.md) | `homelab-mcp.service` on port 9810 (https://mcp.hubris.network) | 14 tools: 8 context (get_host, search_docs, …) + 5 read-only management (get_service_status, tail_log, …) + list_my_secrets | | Secrets-issuance | [apps (105)](../containers/105-apps.md) | `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` | diff --git a/inventory.yaml b/inventory.yaml index b76f253..00c79a0 100644 --- a/inventory.yaml +++ b/inventory.yaml @@ -88,7 +88,7 @@ services: port: 9810 systemd_unit: homelab-mcp public_host: mcp.hubris.network - endpoint: https://mcp.hubris.network/sse + endpoint: https://mcp.hubris.network 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). diff --git a/mcp/server.py b/mcp/server.py index f895991..48897b2 100755 --- a/mcp/server.py +++ b/mcp/server.py @@ -333,4 +333,4 @@ def ping_service(service: str) -> dict: if __name__ == "__main__": - mcp.run(transport="sse") + mcp.run(transport="streamable_http") diff --git a/operations/agent-enrollment.md b/operations/agent-enrollment.md index 1003557..6b0263c 100644 --- a/operations/agent-enrollment.md +++ b/operations/agent-enrollment.md @@ -20,7 +20,7 @@ operational reference is here. | OS is Linux or macOS | bootstrap detects via `uname -s` | `uname -s` | | On the mesh (Netbird or Tailscale) **or** on the LAN | issuance is gated to mesh + LAN subnets. **For Netbird: use a setup-key, not interactive auth** — see "Getting onto Netbird" below. | `netbird status` / `tailscale status` | | `git`, `python3`, `python3-yaml`, `age`, `sops` | bootstrap preflight; `homelab` CLI imports yaml | See per-OS commands below | -| Can resolve `*.hubris.network` | bootstrap calls `https://secrets.hubris.network/issue` and writes `https://mcp.hubris.network/sse` | `dig +short mcp.hubris.network` (should return `192.168.8.175`) | +| Can resolve `*.hubris.network` | bootstrap calls `https://secrets.hubris.network/issue` and writes `https://mcp.hubris.network` | `dig +short mcp.hubris.network` (should return `192.168.8.175`) | ### Hostname mismatch is the most common bootstrap failure @@ -83,7 +83,7 @@ If DNS isn't an option at all, override the URLs at bootstrap time: sudo HOMELAB_GITEA_TOKEN=... \ HOMELAB_REPO_URL=http://192.168.8.121:3000/dtoro/Homelab-Docs.git \ HOMELAB_ISSUANCE_NETBIRD=http://192.168.8.205:9820/issue \ - HOMELAB_MCP_URL=http://192.168.8.205:9810/sse \ + HOMELAB_MCP_URL=http://192.168.8.205:9810 \\ bash /tmp/bootstrap.sh --with-mcp ``` diff --git a/ssh/deploy-keys.sh b/ssh/deploy-keys.sh index 4050944..790a8a8 100644 --- a/ssh/deploy-keys.sh +++ b/ssh/deploy-keys.sh @@ -49,8 +49,15 @@ echo " hubris: $added key(s) added" echo "" echo "= Deploying SSH keys to LXCs =" -# pct list output: VMID Status Lock Name -pct list | tail -n +2 | while read -r vmid status _ name; do +# pct list output: VMID Status Lock Name (Lock may be empty) +# Use awk to grab VMID, Status, and Name (last field) +pct list | tail -n +2 | while read -r line; do + vmid=$(echo "$line" | awk '{print $1}') + status=$(echo "$line" | awk '{print $2}') + name=$(echo "$line" | awk '{print $NF}') + # Skip header if any + [ "$vmid" = "VMID" ] && continue + [ -z "$vmid" ] && continue if [ "$status" != "running" ]; then echo " SKIP $name ($vmid): status=$status" continue From e7bdf9fca710f0f2a5ef5054a4bd37a1626f008f Mon Sep 17 00:00:00 2001 From: dtoro <d.toro.v@pm.me> Date: Tue, 2 Jun 2026 00:12:59 +0200 Subject: [PATCH 15/25] fix: streamable-http (hyphen, not underscore) in FastMCP transport --- mcp/server.py | 2 +- ssh/gen-config.py | 208 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 ssh/gen-config.py diff --git a/mcp/server.py b/mcp/server.py index 48897b2..b08700a 100755 --- a/mcp/server.py +++ b/mcp/server.py @@ -333,4 +333,4 @@ def ping_service(service: str) -> dict: if __name__ == "__main__": - mcp.run(transport="streamable_http") + mcp.run(transport="streamable-http") diff --git a/ssh/gen-config.py b/ssh/gen-config.py new file mode 100644 index 0000000..8a1a5e1 --- /dev/null +++ b/ssh/gen-config.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +"""Generate ~/.ssh/config.d/homelab from inventory.yaml. + +Usage: + python3 ssh/gen-config.py # print to stdout + python3 ssh/gen-config.py --install # write to ~/.ssh/config.d/homelab + +The generated config provides short hostname aliases for every host in +the homelab inventory. LAN IPs are preferred (they work directly on-LAN +and are routed via Netbird 192.168.8.0/24 off-LAN); mesh FQDNs are +available as <name>-mesh fallbacks for roaming workstations. +""" + +from __future__ import annotations + +import argparse +import os +import sys +from pathlib import Path + +try: + import yaml +except ImportError: + print("PyYAML required (pip install pyyaml)", file=sys.stderr) + sys.exit(2) + +INVENTORY_PATH = Path(os.environ.get( + "HOMELAB_CONTEXT_DIR", "/opt/homelab-context" +)) / "inventory.yaml" + +SSH_CONFIG_DIR = Path.home() / ".ssh" / "config.d" +SSH_CONFIG_FILE = SSH_CONFIG_DIR / "homelab" +SSH_MAIN_CONFIG = Path.home() / ".ssh" / "config" +INCLUDE_LINE = f"Include ~/.ssh/config.d/homelab" + +# Hosts to skip in the generated config +SKIP_HOSTS = {"ludo-mini", "authentik"} # offline / not enrolled + + +def inventory() -> dict: + if not INVENTORY_PATH.exists(): + print(f"ERROR: no inventory at {INVENTORY_PATH}", file=sys.stderr) + sys.exit(1) + return yaml.safe_load(INVENTORY_PATH.read_text()) + + +def gen_config() -> str: + inv = inventory() + hosts = inv.get("hosts", {}) + mesh_globals = inv.get("mesh", {}) + + lines: list[str] = [] + lines.append("# Homelab SSH config — generated from inventory.yaml") + lines.append(f"# Source: {INVENTORY_PATH}") + lines.append("# Do not edit by hand. Run: homelab ssh-config") + lines.append("") + + # --- Common defaults (before any Host block) --- + lines.append("# --- Defaults ---") + lines.append("Host *") + lines.append(" IdentityFile ~/.ssh/id_ed25519") + lines.append(" IdentitiesOnly yes") + lines.append(" ServerAliveInterval 30") + lines.append(" StrictHostKeyChecking accept-new") + lines.append("") + + def add_entry( + tag: str, + hostname: str, + user: str = "root", + port: int = 22, + extra_lines: list[str] | None = None, + ) -> None: + lines.append(f"Host {tag}") + lines.append(f" HostName {hostname}") + lines.append(f" User {user}") + if port != 22: + lines.append(f" Port {port}") + if extra_lines: + lines.extend(f" {el}" for el in extra_lines) + lines.append("") + + # Sort hosts by kind for a logical output order + def sort_key(item): + name, h = item + kind = h.get("kind", "") + order = { + "workstation": 0, + "proxmox-host": 1, + "lxc": 2, + "vm": 3, + "external": 4, + } + return (order.get(kind, 9), name) + + sorted_hosts = sorted( + [(n, h) for n, h in hosts.items() if n not in SKIP_HOSTS], + key=sort_key, + ) + + for name, h in sorted_hosts: + kind = h.get("kind", "") + ssh_config = h.get("ssh", {}) or {} + user = ssh_config.get("user", "root") + port = ssh_config.get("port", 22) + lan_ip = h.get("lan_ip") + mesh = h.get("mesh", {}) or {} + nb = mesh.get("netbird", {}) or {} + ts = mesh.get("tailscale", {}) or {} + nb_fqdn = nb.get("fqdn") or nb.get("ip") or "" + nb_port = ssh_config.get("netbird_port") + + # Determine primary address + if nb_port and not lan_ip: + # Hosts with netbird SSH but no LAN IP: mesh-only + primary = nb_fqdn + elif lan_ip: + primary = lan_ip + elif nb_fqdn: + primary = nb_fqdn + elif ts.get("fqdn"): + primary = ts["fqdn"] + else: + continue # no address found + + extra = [] + if kind == "proxmox-host": + extra = [ + "ControlMaster auto", + "ControlPath ~/.ssh/cm/%C", + "ControlPersist 2h", + ] + + add_entry(name, primary, user, port, extra_lines=extra) + + # Mesh fallback for workstations and hubris + if nb_fqdn and (kind == "workstation" or nb_port): + mesh_tag = f"{name}-mesh" + mesh_port = nb_port or port + mesh_extra = [] + if kind == "proxmox-host": + mesh_extra = [ + "ControlMaster auto", + "ControlPath ~/.ssh/cm/%C", + "ControlPersist 2h", + ] + add_entry(mesh_tag, nb_fqdn, user, mesh_port, extra_lines=mesh_extra) + + # --- *.hubris.network ControlMaster --- + lines.append("# --- Mesh ControlMaster (speeds up repeated mesh ops) ---") + lines.append("Host *.netbird.selfhosted") + lines.append(" ControlMaster auto") + lines.append(" ControlPath ~/.ssh/cm/%C") + lines.append(" ControlPersist 2h") + lines.append("") + + return "\n".join(lines) + + +def ensure_include() -> bool: + """Add 'Include ~/.ssh/config.d/homelab' to the main SSH config if missing.""" + if not SSH_MAIN_CONFIG.exists(): + SSH_MAIN_CONFIG.parent.mkdir(parents=True, exist_ok=True) + with open(SSH_MAIN_CONFIG, "w") as f: + f.write(f"{INCLUDE_LINE}\n") + return True + + content = SSH_MAIN_CONFIG.read_text() + for line in content.splitlines(): + stripped = line.strip() + if stripped.startswith("Include") and "homelab" in stripped: + return False # already present + + # Prepend to existing config + updated = f"{INCLUDE_LINE}\n\n{content}" + SSH_MAIN_CONFIG.write_text(updated) + return True + + +def install() -> None: + """Write config to ~/.ssh/config.d/homelab and ensure Include is set.""" + SSH_CONFIG_DIR.mkdir(parents=True, exist_ok=True) + config = gen_config() + SSH_CONFIG_FILE.write_text(config) + SSH_CONFIG_FILE.chmod(0o644) + changed = ensure_include() + print(f"Wrote {SSH_CONFIG_FILE} ({len(config.splitlines())} lines)") + if changed: + print(f"Added '{INCLUDE_LINE}' to {SSH_MAIN_CONFIG}") + print("Done. Run: ssh <hostname> (e.g. ssh gitea)") + + +def main() -> int: + p = argparse.ArgumentParser(description=__doc__) + p.add_argument("--install", "-i", action="store_true", + help=f"write to {SSH_CONFIG_FILE} and wire Include into main config") + args = p.parse_args() + + if args.install: + install() + else: + print(gen_config()) + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From bebebf486ad5c31750f054ad388232e148a3a667 Mon Sep 17 00:00:00 2001 From: dtoro <d.toro.v@pm.me> Date: Tue, 2 Jun 2026 00:16:15 +0200 Subject: [PATCH 16/25] ssh-config: add gen-config.py + homelab ssh-config CLI subcommand --- bin/homelab | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/bin/homelab b/bin/homelab index ee1621c..d1c3a94 100755 --- a/bin/homelab +++ b/bin/homelab @@ -517,6 +517,19 @@ def cmd_ssh(args: argparse.Namespace) -> int: os.execvp(cmd[0], cmd) +def cmd_ssh_config(args: argparse.Namespace) -> int: + """Generate SSH config from inventory.yaml.""" + script = CONTEXT / "ssh" / "gen-config.py" + if not script.exists(): + die(f"ssh-config generator not found: {script}") + + cmd = [sys.executable or "python3", str(script)] + if args.install: + cmd.append("--install") + + return subprocess.call(cmd) + + def cmd_pct(args: argparse.Namespace) -> int: lxc = args.lxc inv = inventory() @@ -1459,6 +1472,12 @@ def main() -> int: sp.add_argument("command", nargs=argparse.REMAINDER) sp.set_defaults(func=cmd_ssh) + sp = sub.add_parser("ssh-config", + help="generate ~/.ssh/config.d/homelab from inventory.yaml") + sp.add_argument("--install", "-i", action="store_true", + help=f"write to ~/.ssh/config.d/homelab and wire Include into main config") + sp.set_defaults(func=cmd_ssh_config) + sp = sub.add_parser("pct", help="proxy pct commands via ssh to hubris") sp.add_argument("lxc") sp.add_argument("action") From c299409431df315c573237f749020616ebda12d9 Mon Sep 17 00:00:00 2001 From: dtoro <d.toro.v@pm.me> Date: Tue, 2 Jun 2026 00:20:28 +0200 Subject: [PATCH 17/25] docs: update ssh-access.md + agent-enrollment.md with universal SSH setup --- infrastructure/ssh-access.md | 199 +++++++++++++++++++++++++++++---- operations/agent-enrollment.md | 77 +++++++++++++ 2 files changed, 254 insertions(+), 22 deletions(-) diff --git a/infrastructure/ssh-access.md b/infrastructure/ssh-access.md index 0cb907b..5a89f04 100644 --- a/infrastructure/ssh-access.md +++ b/infrastructure/ssh-access.md @@ -1,45 +1,200 @@ # SSH access -How to reach hubris and the VPS over SSH, and the dual-server gotcha. +How to reach every host in the fleet from any workstation, with LAN as +the primary path and Netbird as the automatic backup. -## Hubris +## Architecture -Two SSH endpoints — easy to hit the wrong one. +SSH access relies on three layers: -| Server | Listen | Auth | Notes | -| -------------- | ---------------------------- | --------------------------------- | ----- | -| OpenSSH | `0.0.0.0:22` | `authorized_keys` at `/etc/pve/priv/authorized_keys` (Proxmox cluster-synced; symlinked from `/root/.ssh/authorized_keys`) | Standard. | -| Netbird SSH | `100.122.38.109:22022` | OIDC / browser auth — bypasses `authorized_keys` | If a client lands here it'll open a browser tab to authenticate, then sometimes hang. Force port 22 or use the LAN IP. | +1. **Homelab inventory (`inventory.yaml`)** — the single source of truth + for every host's LAN IP, Netbird addresses, SSH user, and port. +2. **Key distribution (`ssh/deploy-keys.sh`)** — deploys workstation SSH + public keys to hubris and every running LXC, so any key-authorized + workstation can log in anywhere. +3. **Config generation (`homelab ssh-config --install`)** — generates + `~/.ssh/config.d/homelab` with short hostname aliases for every host, + using LAN IPs (routed via Netbird's `192.168.8.0/24` subnet route when + off-LAN) with Netbird FQDN fallbacks (`<name>-mesh`) for roaming + workstations. -### Authorized root keys -- `root@hubris` (self, RSA) — original. -- `d.toro.v@pm.me` (ed25519) — user's iMac (`mac-mini.netbird.selfhosted`, LAN `192.168.8.174`), added 2026-04-22. +### How it works -### Notes -- Password auth is enabled on hubris but the root password is **not** the one the user expects. Prefer key flows; don't try `ssh-copy-id` blind. -- Off-LAN access from the iMac uses the LAN path. As of 2026-04-22 the iMac's Netbird tunnel to hubris was P2P healthy but no packets were captured on `wt0`; needs revisit if remote access becomes critical. +- **From on-LAN:** `ssh gitea` resolves to `192.168.8.121` directly. +- **From off-LAN (Netbird):** The same `192.168.8.121` works because + hubris routes the `192.168.8.0/24` subnet through Netbird. +- **Roaming workstations:** `ssh mac-mini-mesh` or `ssh republic-laptop-mesh` + uses the Netbird FQDN as a fallback when the workstation is off its + home subnet. -## VPS (`82.165.190.79` / `100.122.165.149`) +The `homelab ssh <host>` CLI command also has built-in LAN probing: +it tries a 1.5s TCP connect to the LAN IP, and if that fails, falls +back to the Netbird FQDN. -- **Mesh-only.** Public `:22` is dropped by the nftables firewall. SSH reaches the VPS only over `wt0`. -- Key-only (`PasswordAuthentication no`, `PermitRootLogin prohibit-password`) via drop-in at `/etc/ssh/sshd_config.d/10-hubris-hardening.conf`. Original config backed up at `/etc/ssh/sshd_config.bak.<ts>`. -- `ListenAddress` is still 0.0.0.0; gating is firewall-layer. -- Authorized root keys: PVE (`root@hubris`), iMac (`d.toro.v@pm.me`). Add a new device with `ssh-copy-id root@100.122.165.149` from a mesh peer before disabling its access paths. +## Key distribution -See [VPS hardening](vps-hardening.md) for the firewall + fail2ban rules and recovery paths. +Each workstation's SSH public key lives in the repo at: +`ssh/authorized_keys/<hostname>.pub` + +To deploy or re-deploy all workstation keys to hubris + every running LXC: + +```bash +# From hubris (or via homelab pct): +sudo bash /opt/homelab-context/ssh/deploy-keys.sh + +# Or from any workstation: +ssh root@192.168.8.77 "bash /opt/homelab-context/ssh/deploy-keys.sh" +``` + +This script: +- Reads all `.pub` files from `ssh/authorized_keys/` +- Adds any missing keys to `/etc/pve/priv/authorized_keys` on hubris +- For each running LXC, appends keys to `/root/.ssh/authorized_keys` +- Is idempotent — skips keys already present + +## Config generation + +To generate the SSH config on any workstation: + +```bash +homelab ssh-config --install +``` + +This writes to `~/.ssh/config.d/homelab` and ensures +`Include ~/.ssh/config.d/homelab` is present in `~/.ssh/config`. + +The config is regenerated automatically on every `homelab sync` (which +kicks the 5-minute context sync timer). + +## Adding a new workstation + +When onboarding a new machine: + +1. Hostname must match an entry in `inventory.yaml`. +2. If the workstation will be on the LAN, add its `lan_ip` to + `inventory.yaml` and push. This gives it a primary LAN entry in the + generated SSH config. +3. Enable SSH Remote Login: + - **macOS:** `sudo launchctl load -w /System/Library/LaunchDaemons/ssh.plist` + - **Linux:** `sudo systemctl enable --now sshd` +4. Generate an SSH keypair if one doesn't exist: + ```bash + ssh-keygen -t ed25519 -a 100 + ``` +5. Publish the public key to the repo: + ```bash + cp ~/.ssh/id_ed25519.pub /opt/homelab-context/ssh/authorized_keys/<hostname>.pub + cd /opt/homelab-context && git add ssh/authorized_keys/ && git commit -m 'ssh: add <hostname> pubkey' && git push + ``` +6. Deploy the key to all hosts: + ```bash + ssh root@192.168.8.77 "cd /opt/homelab-context && git pull --ff-only && bash ssh/deploy-keys.sh" + ``` +7. Generate the local SSH config: + ```bash + homelab ssh-config --install + ``` + +## Hosts + +### Hubris (PVE host) + +| Detail | Value | +|--------|-------| +| LAN IP | `192.168.8.77` | +| Netbird | `100.122.38.109` (FQDN: `proxmox-server.netbird.selfhosted`) | +| Netbird SSH port | `22022` (mesh-only, OIDC auth) | +| SSH user | `root` | +| Authorized keys | `/etc/pve/priv/authorized_keys` (Proxmox cluster-synced) | + +Authorized root keys currently deployed: +- `root@hubris` (self, RSA) +- `d.toro.v@pm.me` (ed25519) — mac-mini + +### LXCs + +Every LXC at `192.168.8.x` accepts root SSH via authorized_keys. Keys +are managed by `ssh/deploy-keys.sh`. SSH user is `root`. + +| LXC | Name | LAN IP | Role | +|-----|------|--------|------| +| 101 | jellyfin | `192.168.8.206` | media-server | +| 102 | nfs-export | `192.168.8.200` | storage-export | +| 103 | paperless | `192.168.8.130` | document-archive | +| 104 | gitea | `192.168.8.121` | git-server | +| 105 | apps | `192.168.8.205` | docker-apps | +| 106 | auth-outpost | `192.168.8.184` | authentik-outpost | +| 107 | dns | `192.168.8.185` | dns-helper | +| 114 | nextcloud | `192.168.8.224` | file-sync | +| 118 | elementsynapse | `192.168.8.239` | matrix-server | +| 119 | sophia | `192.168.8.157` | workshop | +| 120 | mule-images | `192.168.8.136` | photo-management | +| 121 | caddy | `192.168.8.175` | reverse-proxy | +| 122 | arriman | `192.168.8.132` | arr-stack | +| 123 | claudio-bot | `192.168.8.230` | matrix-agent | +| 126 | plato | `192.168.8.190` | app | + +### Workstations + +| Name | OS | LAN IP | Netbird FQDN | SSH user | +|------|----|--------|--------------|----------| +| mac-mini | macOS | `192.168.8.174` | `mac-mini-234-17.netbird.selfhosted` | `dtoro` | +| republic-laptop | Linux | TBD | `republic-laptop.netbird.selfhosted` | `dtoro` | +| ludo-mini | Linux | `192.168.8.133` | `ludo-mini.netbird.selfhosted` | TBD | + +### VPS (external) + +| Detail | Value | +|--------|-------| +| Public IP | `82.165.190.79` | +| Netbird | `100.122.165.149` (FQDN: `netbird-ionos.netbird.selfhosted`) | +| SSH user | `root` | +| Access | Mesh-only — public port 22 is blocked by nftables. Key-only auth. | + +## VPS + +Access is mesh-only. From a mesh-connected peer: + +```bash +ssh root@100.122.165.149 +ssh root@netbird-ionos.netbird.selfhosted +# or via homelab: +homelab ssh netbird-vps +``` + +## Verification + +```bash +# From any workstation after running homelab ssh-config --install: +for name in hubris gitea apps sophia paperless caddy jellyfin nextcloud; do + ssh -o BatchMode=yes "$name" "hostname" && echo "$name OK" +done +``` ## Related -- [Hubris host](../hosts/hubris.md) + - [Mesh migration](mesh.md) - [VPS hardening](vps-hardening.md) +- [Agent enrollment](../operations/agent-enrollment.md) +- [Homelab CLI](../bin/homelab) ## Changelog +### 2026-06-02 — universal SSH reachability + +Replaced ad-hoc per-workstation SSH configs with inventory-generated +configs (`ssh/gen-config.py`, `homelab ssh-config`). Added centralized +key distribution (`ssh/deploy-keys.sh`, `ssh/authorized_keys/`). All +LXCs now accept root SSH from any workstation whose pubkey is in the +repo. mac-mini Remote Login enabled. Netbird subnet route +(192.168.8.0/24 via hubris) provides off-LAN reachability for all LAN +IPs. + ### 2026-04-28 — wiki entry created Initial documentation. ### 2026-04-23 — VPS SSH hardened to mesh-only -Public `:22` blocked at nftables. Key-only sshd. See [VPS hardening](vps-hardening.md). +Public `:22` blocked at nftables. Key-only sshd. ### 2026-04-22 — iMac key authorized on hubris -`d.toro.v@pm.me` added to `/etc/pve/priv/authorized_keys`. +`d.toro.v@pm.me` added to `/etc/pve/priv/authorized_keys`. \ No newline at end of file diff --git a/operations/agent-enrollment.md b/operations/agent-enrollment.md index 6b0263c..d48a2a3 100644 --- a/operations/agent-enrollment.md +++ b/operations/agent-enrollment.md @@ -163,6 +163,77 @@ For Claude Code: start a new session — the `homelab` MCP server appears in `~/.claude/.mcp.json` and registers 14 tools (8 context, 5 management, 1 secrets-metadata). +## Post-bootstrap: SSH reachability + +A new workstation must be reachable from other workstations and must be +able to reach every host by short hostname. Run these steps after the +bootstrap verify passes: + +### 1. Enable SSH server + +```bash +# macOS: +sudo launchctl load -w /System/Library/LaunchDaemons/ssh.plist + +# Linux: +sudo systemctl enable --now sshd +``` + +### 2. Generate SSH key (if missing) + +```bash +ls ~/.ssh/id_ed25519.pub 2>/dev/null || ssh-keygen -t ed25519 -a 100 +``` + +### 3. Publish pubkey to the repo + +```bash +cp ~/.ssh/id_ed25519.pub /opt/homelab-context/ssh/authorized_keys/$(hostname -s).pub +cd /opt/homelab-context && git add ssh/authorized_keys/ && git commit -m 'ssh: add $(hostname -s) pubkey' && git push +``` + +### 4. Deploy keys to all hosts + +From any existing enrolled machine (hubris or another workstation): + +```bash +ssh root@192.168.8.77 "cd /opt/homelab-context && git pull --ff-only && bash ssh/deploy-keys.sh" +``` + +This adds the new workstation's pubkey to hubris and every running LXC. + +### 5. Generate SSH config + +```bash +homelab ssh-config --install +``` + +Verify: + +```bash +ssh hubris hostname # should return "hubris" without password +ssh gitea hostname # should return "gitea" without password +ssh mac-mini hostname # should return "mac-mini" without password (workstation-to-workstation) +``` + +### 6. Add LAN IP to inventory (if on LAN) + +If the workstation has a static or reserved LAN IP, add it to +`inventory.yaml`: + +```yaml +hosts: + your-hostname: + lan_ip: 192.168.8.xxx +``` + +This gives it a primary LAN entry in the generated SSH config (faster +than the Netbird fallback). Commit + push, then: + +```bash +cd /opt/homelab-context && git pull --ff-only && homelab ssh-config --install +``` + ## Claude Code permissions for fleet ops By default Claude Code's auto-mode classifier asks for confirmation on every @@ -278,6 +349,12 @@ The CLI prints a follow-up checklist that the operator must do manually: ## Changelog +### 2026-06-02 — SSH reachability post-bootstrap steps +Added a new "Post-bootstrap: SSH reachability" section covering SSH key +generation, pubkey publication, deployment to hosts, SSH config generation, +and LAN IP registration. New workstations enrolled via this doc will +automatically join the universal SSH mesh. + ### 2026-05-31 — cross-link to hermes-agent.md Added a sibling page covering Nous-Hermes-on-Goose enrollment ([hermes-agent.md](./hermes-agent.md)) and noted it at the top of this page. The Hermes flow extends `bootstrap.sh` with `--with-hermes` and `homelab client add` with the same flag; it does not change the underlying enrollment steps documented here. From ec48041b54e873d9a578028d24fdcd2b5a14c7b0 Mon Sep 17 00:00:00 2001 From: dtoro <d.toro.v@pm.me> Date: Tue, 2 Jun 2026 00:25:34 +0200 Subject: [PATCH 18/25] fix: correct endpoint path to /mcp for streamable-http transport --- bootstrap.sh | 4 +- containers/105-apps.md | 2 +- hosts/apps.yaml | 4 +- hosts/arriman.yaml | 2 +- hosts/authentik.yaml | 2 +- hosts/caddy.yaml | 2 +- hosts/claudio-bot.yaml | 2 +- hosts/elementsynapse.yaml | 2 +- hosts/gitea.yaml | 2 +- hosts/haos.yaml | 2 +- hosts/hubris.yaml | 2 +- hosts/jellyfin.yaml | 2 +- hosts/ludo-mini.yaml | 2 +- hosts/mac-mini.yaml | 2 +- hosts/mule-images.yaml | 2 +- hosts/netbird-vps.yaml | 2 +- hosts/nextcloud.yaml | 2 +- hosts/nfs-export.yaml | 2 +- hosts/paperless.yaml | 2 +- hosts/plato.yaml | 2 +- hosts/republic-laptop.yaml | 2 +- hosts/sophia.yaml | 2 +- hosts/zimaos.yaml | 2 +- infrastructure/homelab-context.md | 2 +- inventory.yaml | 2 +- tools/caveman/caveman.js | 23 ++++++ tools/caveman/caveman_wrapper.sh | 76 +++++++++++++++++++ tools/caveman/templates/git_log.txt | 3 + tools/caveman/templates/git_status.txt | 12 +++ tools/caveman/templates/test_results.txt | 4 + .../network.hubris.homelab-context-sync.plist | 27 +++++++ tools/post-pull.sh | 36 +++++++++ tools/setup-caveman.sh | 72 ++++++++++++++++++ 33 files changed, 280 insertions(+), 27 deletions(-) create mode 100644 tools/caveman/caveman.js create mode 100644 tools/caveman/caveman_wrapper.sh create mode 100644 tools/caveman/templates/git_log.txt create mode 100644 tools/caveman/templates/git_status.txt create mode 100644 tools/caveman/templates/test_results.txt create mode 100644 tools/network.hubris.homelab-context-sync.plist create mode 100644 tools/post-pull.sh create mode 100644 tools/setup-caveman.sh diff --git a/bootstrap.sh b/bootstrap.sh index 090af9b..643171f 100755 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -23,8 +23,8 @@ REPO_HTTPS="${HOMELAB_REPO_URL:-https://git.hubris.network/dtoro/Homelab-Docs.gi CLONE_DIR="${HOMELAB_CONTEXT_DIR:-/opt/homelab-context}" ISSUANCE_URL_NETBIRD="${HOMELAB_ISSUANCE_NETBIRD:-https://secrets.hubris.network/issue}" ISSUANCE_URL_TAILSCALE="${HOMELAB_ISSUANCE_TAILSCALE:-https://secrets.hubris.network/issue}" -MCP_URL="${HOMELAB_MCP_URL:-https://mcp.hubris.network}" -HERMES_MCP_URI="${HOMELAB_HERMES_MCP_URI:-https://mcp.hubris.network}" +MCP_URL="${HOMELAB_MCP_URL:-https://mcp.hubris.network/mcp}" +HERMES_MCP_URI="${HOMELAB_HERMES_MCP_URI:-https://mcp.hubris.network/mcp}" HERMES_MODEL="${HOMELAB_HERMES_MODEL:-nousresearch/hermes-4-405b}" WITH_MCP=0 diff --git a/containers/105-apps.md b/containers/105-apps.md index f277e61..1477f10 100644 --- a/containers/105-apps.md +++ b/containers/105-apps.md @@ -60,7 +60,7 @@ is `dtoro/Homelab-Docs/mcp/server.py`; service unit disabled at the FastMCP layer because mesh+LAN gating is the actual trust boundary. -- Endpoint: `https://mcp.hubris.network` (Caddy → `:9810`). StreamableHTTP transport (POST `/`). +- Endpoint: `https://mcp.hubris.network/mcp` (Caddy → `:9810`). StreamableHTTP transport (POST `/mcp`). - 14 tools registered: `get_host`, `list_services`, `find_service`, `get_topology`, `search_docs`, `get_page`, `get_changelog`, `whoami`, `list_my_secrets` (context); `get_service_status`, `tail_log`, diff --git a/hosts/apps.yaml b/hosts/apps.yaml index aa76910..7e3ee4c 100644 --- a/hosts/apps.yaml +++ b/hosts/apps.yaml @@ -38,7 +38,7 @@ services_hosted: port: 9810 systemd_unit: homelab-mcp public_host: mcp.hubris.network - endpoint: https://mcp.hubris.network + endpoint: https://mcp.hubris.network/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). - name: secrets_issuance @@ -51,5 +51,5 @@ services_hosted: age_pubkey: age1duyl8mkpgu80uv934dy8q7enqjms6yvdz264hme8uryuxmvvqesq6rusq0 see_also: - containers/105-apps.md -mcp_endpoint: https://mcp.hubris.network +mcp_endpoint: https://mcp.hubris.network/mcp secrets_issuance_endpoint: https://secrets.hubris.network/issue diff --git a/hosts/arriman.yaml b/hosts/arriman.yaml index a8dd7d6..15cdede 100644 --- a/hosts/arriman.yaml +++ b/hosts/arriman.yaml @@ -30,5 +30,5 @@ services_hosted: note: jellyseerr / qbit / sab on docker compose see_also: - containers/122-arriman.md -mcp_endpoint: https://mcp.hubris.network +mcp_endpoint: https://mcp.hubris.network/mcp secrets_issuance_endpoint: https://secrets.hubris.network/issue diff --git a/hosts/authentik.yaml b/hosts/authentik.yaml index 8e98999..5a050bc 100644 --- a/hosts/authentik.yaml +++ b/hosts/authentik.yaml @@ -28,5 +28,5 @@ notes: - 'Also hosts split-horizon dnsmasq: /etc/dnsmasq.d/hubris-split.conf' see_also: - containers/124-authentik.md -mcp_endpoint: https://mcp.hubris.network +mcp_endpoint: https://mcp.hubris.network/mcp secrets_issuance_endpoint: https://secrets.hubris.network/issue diff --git a/hosts/caddy.yaml b/hosts/caddy.yaml index 12537f7..cc0adcc 100644 --- a/hosts/caddy.yaml +++ b/hosts/caddy.yaml @@ -28,5 +28,5 @@ notes: - /etc/caddy is a git checkout of dtoro/caddy-conf see_also: - containers/121-caddy.md -mcp_endpoint: https://mcp.hubris.network +mcp_endpoint: https://mcp.hubris.network/mcp secrets_issuance_endpoint: https://secrets.hubris.network/issue diff --git a/hosts/claudio-bot.yaml b/hosts/claudio-bot.yaml index a0fb3bd..bfce5bd 100644 --- a/hosts/claudio-bot.yaml +++ b/hosts/claudio-bot.yaml @@ -18,5 +18,5 @@ notes: age_pubkey: age1xmkeq968areza2necqyq0065dpeegngzyr6dhagh0n6pl33lccfqe5mqn9 see_also: - containers/123-claudio-bot.md -mcp_endpoint: https://mcp.hubris.network +mcp_endpoint: https://mcp.hubris.network/mcp secrets_issuance_endpoint: https://secrets.hubris.network/issue diff --git a/hosts/elementsynapse.yaml b/hosts/elementsynapse.yaml index c2b4f51..a162061 100644 --- a/hosts/elementsynapse.yaml +++ b/hosts/elementsynapse.yaml @@ -25,5 +25,5 @@ services_hosted: backend: elementsynapse see_also: - containers/118-elementsynapse.md -mcp_endpoint: https://mcp.hubris.network +mcp_endpoint: https://mcp.hubris.network/mcp secrets_issuance_endpoint: https://secrets.hubris.network/issue diff --git a/hosts/gitea.yaml b/hosts/gitea.yaml index dde53fb..87f25a2 100644 --- a/hosts/gitea.yaml +++ b/hosts/gitea.yaml @@ -30,5 +30,5 @@ notes: - Bare repos live at /mnt/library/repos/dtoro/*.git see_also: - containers/104-gitea.md -mcp_endpoint: https://mcp.hubris.network +mcp_endpoint: https://mcp.hubris.network/mcp secrets_issuance_endpoint: https://secrets.hubris.network/issue diff --git a/hosts/haos.yaml b/hosts/haos.yaml index 08227bd..40e3e9c 100644 --- a/hosts/haos.yaml +++ b/hosts/haos.yaml @@ -23,5 +23,5 @@ services_hosted: backend: haos see_also: - vms/108-haos.md -mcp_endpoint: https://mcp.hubris.network +mcp_endpoint: https://mcp.hubris.network/mcp secrets_issuance_endpoint: https://secrets.hubris.network/issue diff --git a/hosts/hubris.yaml b/hosts/hubris.yaml index 877d61e..bb156ce 100644 --- a/hosts/hubris.yaml +++ b/hosts/hubris.yaml @@ -31,5 +31,5 @@ services_hosted: age_pubkey: age1xkklkvnk5z0fsnh6cfgv70hy9ksfy8rdprwerzw4yk3p4p7cxcqs2yvpz6 see_also: - hosts/hubris.md -mcp_endpoint: https://mcp.hubris.network +mcp_endpoint: https://mcp.hubris.network/mcp secrets_issuance_endpoint: https://secrets.hubris.network/issue diff --git a/hosts/jellyfin.yaml b/hosts/jellyfin.yaml index b50c70d..c95f5a1 100644 --- a/hosts/jellyfin.yaml +++ b/hosts/jellyfin.yaml @@ -27,5 +27,5 @@ services_hosted: backend: jellyfin see_also: - containers/101-jellyfin.md -mcp_endpoint: https://mcp.hubris.network +mcp_endpoint: https://mcp.hubris.network/mcp secrets_issuance_endpoint: https://secrets.hubris.network/issue diff --git a/hosts/ludo-mini.yaml b/hosts/ludo-mini.yaml index e3f651b..cc44feb 100644 --- a/hosts/ludo-mini.yaml +++ b/hosts/ludo-mini.yaml @@ -13,5 +13,5 @@ mesh_globals: accepted: - netbird - tailscale -mcp_endpoint: https://mcp.hubris.network +mcp_endpoint: https://mcp.hubris.network/mcp secrets_issuance_endpoint: https://secrets.hubris.network/issue diff --git a/hosts/mac-mini.yaml b/hosts/mac-mini.yaml index 416df8b..829ebd6 100644 --- a/hosts/mac-mini.yaml +++ b/hosts/mac-mini.yaml @@ -19,5 +19,5 @@ ssh: notes: - Only macOS in the fleet. Bootstrap uses launchd. age_pubkey: age1z62ff2ak9zj5ctcvaxwyyhedwjvlwgm2dkn9nk3wrwk8fkavcpmsqwc2vs -mcp_endpoint: https://mcp.hubris.network +mcp_endpoint: https://mcp.hubris.network/mcp secrets_issuance_endpoint: https://secrets.hubris.network/issue diff --git a/hosts/mule-images.yaml b/hosts/mule-images.yaml index fd3f4dc..75b8633 100644 --- a/hosts/mule-images.yaml +++ b/hosts/mule-images.yaml @@ -27,5 +27,5 @@ services_hosted: backend: mule-images see_also: - containers/120-mule-images.md -mcp_endpoint: https://mcp.hubris.network +mcp_endpoint: https://mcp.hubris.network/mcp secrets_issuance_endpoint: https://secrets.hubris.network/issue diff --git a/hosts/netbird-vps.yaml b/hosts/netbird-vps.yaml index 13d026c..713a0bb 100644 --- a/hosts/netbird-vps.yaml +++ b/hosts/netbird-vps.yaml @@ -25,5 +25,5 @@ notes: - 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. -mcp_endpoint: https://mcp.hubris.network +mcp_endpoint: https://mcp.hubris.network/mcp secrets_issuance_endpoint: https://secrets.hubris.network/issue diff --git a/hosts/nextcloud.yaml b/hosts/nextcloud.yaml index 16ff6c3..a0f5a39 100644 --- a/hosts/nextcloud.yaml +++ b/hosts/nextcloud.yaml @@ -27,5 +27,5 @@ services_hosted: backend: nextcloud see_also: - containers/114-nextcloud.md -mcp_endpoint: https://mcp.hubris.network +mcp_endpoint: https://mcp.hubris.network/mcp secrets_issuance_endpoint: https://secrets.hubris.network/issue diff --git a/hosts/nfs-export.yaml b/hosts/nfs-export.yaml index 28ebe8c..a38c1b4 100644 --- a/hosts/nfs-export.yaml +++ b/hosts/nfs-export.yaml @@ -15,5 +15,5 @@ mesh_globals: - tailscale see_also: - containers/102-nfs-export.md -mcp_endpoint: https://mcp.hubris.network +mcp_endpoint: https://mcp.hubris.network/mcp secrets_issuance_endpoint: https://secrets.hubris.network/issue diff --git a/hosts/paperless.yaml b/hosts/paperless.yaml index 1fb426d..71b5372 100644 --- a/hosts/paperless.yaml +++ b/hosts/paperless.yaml @@ -27,5 +27,5 @@ services_hosted: backend: paperless see_also: - containers/103-paperless.md -mcp_endpoint: https://mcp.hubris.network +mcp_endpoint: https://mcp.hubris.network/mcp secrets_issuance_endpoint: https://secrets.hubris.network/issue diff --git a/hosts/plato.yaml b/hosts/plato.yaml index dc7169d..e391ff2 100644 --- a/hosts/plato.yaml +++ b/hosts/plato.yaml @@ -24,5 +24,5 @@ services_hosted: backend: plato see_also: - containers/126-plato.md -mcp_endpoint: https://mcp.hubris.network +mcp_endpoint: https://mcp.hubris.network/mcp secrets_issuance_endpoint: https://secrets.hubris.network/issue diff --git a/hosts/republic-laptop.yaml b/hosts/republic-laptop.yaml index f975a78..23fad6a 100644 --- a/hosts/republic-laptop.yaml +++ b/hosts/republic-laptop.yaml @@ -15,5 +15,5 @@ mesh_globals: - tailscale ssh: user: dtoro -mcp_endpoint: https://mcp.hubris.network +mcp_endpoint: https://mcp.hubris.network/mcp secrets_issuance_endpoint: https://secrets.hubris.network/issue diff --git a/hosts/sophia.yaml b/hosts/sophia.yaml index 64774aa..3f28389 100644 --- a/hosts/sophia.yaml +++ b/hosts/sophia.yaml @@ -20,5 +20,5 @@ mounts: - /mnt/library see_also: - containers/119-sophia.md -mcp_endpoint: https://mcp.hubris.network +mcp_endpoint: https://mcp.hubris.network/mcp secrets_issuance_endpoint: https://secrets.hubris.network/issue diff --git a/hosts/zimaos.yaml b/hosts/zimaos.yaml index 7020217..8b2a553 100644 --- a/hosts/zimaos.yaml +++ b/hosts/zimaos.yaml @@ -22,5 +22,5 @@ services_hosted: backend: zimaos see_also: - vms/100-zimaos.md -mcp_endpoint: https://mcp.hubris.network +mcp_endpoint: https://mcp.hubris.network/mcp secrets_issuance_endpoint: https://secrets.hubris.network/issue diff --git a/infrastructure/homelab-context.md b/infrastructure/homelab-context.md index b839b03..c147607 100644 --- a/infrastructure/homelab-context.md +++ b/infrastructure/homelab-context.md @@ -16,7 +16,7 @@ page is the architecture reference. | Per-client clone | every enrolled client | `/opt/homelab-context/` | Read by `homelab` CLI, MCP server, claudio-bot | | `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)](../containers/105-apps.md) | `homelab-mcp.service` on port 9810 (https://mcp.hubris.network) | 14 tools: 8 context (get_host, search_docs, …) + 5 read-only management (get_service_status, tail_log, …) + list_my_secrets | +| MCP server | [apps (105)](../containers/105-apps.md) | `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)](../containers/105-apps.md) | `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` | diff --git a/inventory.yaml b/inventory.yaml index 00c79a0..9d1e724 100644 --- a/inventory.yaml +++ b/inventory.yaml @@ -88,7 +88,7 @@ services: port: 9810 systemd_unit: homelab-mcp public_host: mcp.hubris.network - endpoint: https://mcp.hubris.network + endpoint: https://mcp.hubris.network/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). diff --git a/tools/caveman/caveman.js b/tools/caveman/caveman.js new file mode 100644 index 0000000..6f17d44 --- /dev/null +++ b/tools/caveman/caveman.js @@ -0,0 +1,23 @@ +#!/usr/bin/env node +// Caveman template renderer — reads template + data JSON files and renders output +// Installed automatically via homelab-context post-pull hook +const caveman = require("caveman"); +const fs = require("fs"); +const path = require("path"); + +const args = process.argv.slice(2); +if (args.length === 0) { + console.error("Usage: caveman <template> [data.json]"); + process.exit(1); +} + +const templatePath = args[0]; +let data = {}; +if (args.length > 1) { + data = JSON.parse(fs.readFileSync(args[1], "utf8")); +} + +const template = fs.readFileSync(templatePath, "utf8"); +const templateName = path.basename(templatePath, path.extname(templatePath)); +caveman.register(templateName, template); +console.log(caveman.render(templateName, data).trim()); \ No newline at end of file diff --git a/tools/caveman/caveman_wrapper.sh b/tools/caveman/caveman_wrapper.sh new file mode 100644 index 0000000..3690996 --- /dev/null +++ b/tools/caveman/caveman_wrapper.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# Caveman + RTK Wrapper - Automated token-efficient output formatting +# Installed automatically via homelab-context post-pull hook +# Source: https://github.com/adityahimaone/hermes-agent-rtk-caveman +# Usage: caveman_wrapper.sh <workflow> [options] + +set -e + +WORKFLOW="$1" +shift + +CAVEMAN=~/bin/caveman +TEMPLATES_DIR=~/templates +DATA_DIR=/tmp/caveman_data + +mkdir -p "$DATA_DIR" + +case "$WORKFLOW" in + git-status) + git status --porcelain | awk ' + BEGIN { staged=0; modified=0; untracked=0; deleted=0 } + /^[MARC]./ { staged_arr[staged++] = substr($0, 4) } + /^.[MARC]/ { modified_arr[modified++] = substr($0, 4) } + /^\?\?/ { untracked_arr[untracked++] = substr($0, 4) } + /^D/ || /^.D/ { deleted_arr[deleted++] = substr($0, 4) } + END { + printf "{" + printf "\"staged\":[" + for(i=0;i<staged;i++) printf "%s\"%s\"", (i>0?",":""), staged_arr[i] + printf "],\"modified\":[" + for(i=0;i<modified;i++) printf "%s\"%s\"", (i>0?",":""), modified_arr[i] + printf "],\"untracked\":[" + for(i=0;i<untracked;i++) printf "%s\"%s\"", (i>0?",":""), untracked_arr[i] + printf "],\"deleted\":[" + for(i=0;i<deleted;i++) printf "%s\"%s\"", (i>0?",":""), deleted_arr[i] + printf "]}" + }' > "$DATA_DIR/git_status.json" + + if command -v rtk &>/dev/null; then + rtk "$CAVEMAN" "$TEMPLATES_DIR/git_status.txt" "$DATA_DIR/git_status.json" + else + node "$CAVEMAN" "$TEMPLATES_DIR/git_status.txt" "$DATA_DIR/git_status.json" + fi + ;; + + git-log) + LIMIT="${1:-10}" + git log --oneline -"$LIMIT" --format='{"hash":"%h","author":"%an","date":"%ad","message":"%s"}' --date=short | \ + jq -s '.' > "$DATA_DIR/git_log.json" + jq '{commits: .}' "$DATA_DIR/git_log.json" > "$DATA_DIR/git_log_final.json" + if command -v rtk &>/dev/null; then + rtk "$CAVEMAN" "$TEMPLATES_DIR/git_log.txt" "$DATA_DIR/git_log_final.json" + else + node "$CAVEMAN" "$TEMPLATES_DIR/git_log.txt" "$DATA_DIR/git_log_final.json" + fi + ;; + + test-results) + TEST_CMD="${1:-npx vitest run}" + $TEST_CMD --reporter json 2>/dev/null | \ + jq '{total: .numTotalTests, passed: .numPassedTests, failed: .numFailedTests, suites: [.testResults[] | {name: .name, status: .status, duration: .duration}]}' > "$DATA_DIR/test_results.json" || true + if command -v rtk &>/dev/null; then + rtk "$CAVEMAN" "$TEMPLATES_DIR/test_results.txt" "$DATA_DIR/test_results.json" + else + node "$CAVEMAN" "$TEMPLATES_DIR/test_results.txt" "$DATA_DIR/test_results.json" + fi + ;; + + *) + echo "Usage: caveman_wrapper.sh <workflow> [options]" + echo " git-status - Compact git status" + echo " git-log [limit] - Recent git commits" + echo " test-results [cmd] - Compact test results" + exit 1 + ;; +esac \ No newline at end of file diff --git a/tools/caveman/templates/git_log.txt b/tools/caveman/templates/git_log.txt new file mode 100644 index 0000000..e129fb6 --- /dev/null +++ b/tools/caveman/templates/git_log.txt @@ -0,0 +1,3 @@ +Recent Commits: +{{- for d.commits as commit }} {{commit.hash}} {{commit.date}} {{commit.message}} +{{- end }} \ No newline at end of file diff --git a/tools/caveman/templates/git_status.txt b/tools/caveman/templates/git_status.txt new file mode 100644 index 0000000..d13e2f9 --- /dev/null +++ b/tools/caveman/templates/git_status.txt @@ -0,0 +1,12 @@ +{{- if d.staged }}Staged: +{{- for d.staged as file }} + {{file}} +{{- end }}{{- end }} +{{- if d.modified }}Modified: +{{- for d.modified as file }} ~ {{file}} +{{- end }}{{- end }} +{{- if d.untracked }}Untracked: +{{- for d.untracked as file }} ? {{file}} +{{- end }}{{- end }} +{{- if d.deleted }}Deleted: +{{- for d.deleted as file }} - {{file}} +{{- end }}{{- end }} \ No newline at end of file diff --git a/tools/caveman/templates/test_results.txt b/tools/caveman/templates/test_results.txt new file mode 100644 index 0000000..92b4d5d --- /dev/null +++ b/tools/caveman/templates/test_results.txt @@ -0,0 +1,4 @@ +{{- if d.failed }}Tests: {{d.passed}}/{{d.total}} passed ({{d.failed}} failed) +{{- for d.suites as suite }}{{- if suite.status == "failed" }} {{suite.name}} ({{suite.duration}}ms) +{{- end }}{{- end }}{{- else }}All {{d.total}} tests passed +{{- end }} \ No newline at end of file diff --git a/tools/network.hubris.homelab-context-sync.plist b/tools/network.hubris.homelab-context-sync.plist new file mode 100644 index 0000000..92b043e --- /dev/null +++ b/tools/network.hubris.homelab-context-sync.plist @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>Label</key> + <string>network.hubris.homelab-context-sync</string> + + <key>ProgramArguments</key> + <array> + <string>/usr/bin/env</string> + <string>bash</string> + <string>/opt/homelab-context/tools/post-pull.sh</string> + </array> + + <key>StartInterval</key> + <integer>300</integer> + + <key>RunAtLoad</key> + <true/> + + <key>StandardOutPath</key> + <string>/tmp/homelab-sync.log</string> + + <key>StandardErrorPath</key> + <string>/tmp/homelab-sync.log</string> +</dict> +</plist> diff --git a/tools/post-pull.sh b/tools/post-pull.sh new file mode 100644 index 0000000..8eaccc5 --- /dev/null +++ b/tools/post-pull.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# post-pull.sh — homelab-context sync hook. +# Replaces raw `git pull` in the launchd/systemd timer. +# Runs after every git pull to auto-setup tools from the repo. +# +# Convention: any script at tools/*.setup.sh is sourced/exec'd after pull. +# This lets us ship new tooling to all agent hosts via a simple git push. + +set -euo pipefail + +CONTEXT_DIR="${HOMELAB_CONTEXT_DIR:-/opt/homelab-context}" + +# Git safe.directory workaround: the repo is owned by the regular user +# but launchd runs as root. Run git as the repo owner. +REPO_OWNER=$(stat -f "%Su" "$CONTEXT_DIR" 2>/dev/null || echo "root") + +# 1. Git pull (as repo owner to avoid safe.directory issues) +if [ "$REPO_OWNER" != "root" ] && command -v sudo &>/dev/null; then + sudo -u "$REPO_OWNER" git -C "$CONTEXT_DIR" pull --ff-only --quiet 2>&1 || \ + echo "[post-pull] git pull failed (network?) continuing..." +else + git -C "$CONTEXT_DIR" pull --ff-only --quiet 2>&1 || \ + echo "[post-pull] git pull failed (network?) continuing..." +fi + +# 2. Run any auto-setup scripts +for setup_script in "$CONTEXT_DIR"/tools/*.setup.sh; do + [ -f "$setup_script" ] || continue + echo "[post-pull] running $setup_script..." + bash "$setup_script" || echo "[post-pull] WARNING: $setup_script exited with code $?" +done + +# 3. Symlink AGENTS.md for agent orientation (macOS) +if [ "$(uname)" = "Darwin" ]; then + ln -sf "$CONTEXT_DIR/AGENTS.md" /etc/AGENTS.md 2>/dev/null || true +fi \ No newline at end of file diff --git a/tools/setup-caveman.sh b/tools/setup-caveman.sh new file mode 100644 index 0000000..d5d1365 --- /dev/null +++ b/tools/setup-caveman.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# setup-caveman.sh — idempotent auto-installer for Caveman + RTK token optimization. +# Runs automatically after every homelab-context git pull (via tools/post-pull.sh). +# +# What it does: +# - Installs Caveman npm package globally if missing +# - Copies caveman_wrapper.sh → ~/bin/ +# - Copies caveman.js wrapper → ~/bin/caveman (CLI entry point) +# - Copies templates → ~/templates/ +# - Creates ~/bin/ and ~/templates/ dirs if missing +# - All operations are idempotent (safe to re-run) +# +# Works on: macOS (Homebrew node) and Linux (system node) + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CAVEMAN_DIR="$SCRIPT_DIR/caveman" +BIN_DIR="$HOME/bin" +TEMPLATES_DIR="$HOME/templates" + +# Colors for output (only when connected to a terminal) +if [ -t 1 ]; then + GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m' +else + GREEN=''; YELLOW=''; BLUE=''; NC='' +fi + +log() { echo -e "${BLUE}[caveman]${NC} $1"; } +ok() { echo -e "${GREEN}[caveman]${NC} $1"; } +skip() { echo -e "${YELLOW}[caveman]${NC} $1"; } + +# --- 1. Check / install node + npm --- +if ! command -v node &>/dev/null; then + echo "[caveman] node.js not found — skipping caveman install" + exit 0 +fi + +# --- 2. Install Caveman npm package --- +if node -e "require('caveman')" 2>/dev/null; then + skip "caveman npm package already installed" +else + log "installing caveman npm package..." + npm install -g caveman 2>&1 | tail -1 + ok "caveman npm package installed" +fi + +# --- 3. Create target directories --- +mkdir -p "$BIN_DIR" "$TEMPLATES_DIR" + +# --- 4. Install wrapper script --- +install -m 755 "$CAVEMAN_DIR/caveman_wrapper.sh" "$BIN_DIR/caveman_wrapper.sh" +ok "caveman_wrapper.sh → $BIN_DIR/caveman_wrapper.sh" + +# --- 5. Install caveman CLI wrapper --- +install -m 755 "$CAVEMAN_DIR/caveman.js" "$BIN_DIR/caveman" +ok "caveman.js → $BIN_DIR/caveman" + +# --- 6. Install templates --- +for tmpl in "$CAVEMAN_DIR/templates/"*.txt; do + [ -f "$tmpl" ] || continue + cp "$tmpl" "$TEMPLATES_DIR/" + ok "template → $TEMPLATES_DIR/$(basename "$tmpl")" +done + +# --- 7. Verify --- +if [ -x "$BIN_DIR/caveman_wrapper.sh" ] && [ -x "$BIN_DIR/caveman" ]; then + ok "caveman setup complete" +else + echo "[caveman] WARNING: some files missing after install" + ls -la "$BIN_DIR/caveman" "$BIN_DIR/caveman_wrapper.sh" 2>&1 +fi \ No newline at end of file From 520a86c986f25dcf6563fc33b7ab306646d86938 Mon Sep 17 00:00:00 2001 From: dtoro <d.toro.v@pm.me> Date: Tue, 2 Jun 2026 00:28:43 +0200 Subject: [PATCH 19/25] fix: agent-enrollment.md doc references to use /mcp path --- operations/agent-enrollment.md | 4 ++-- tools/post-pull.sh | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/operations/agent-enrollment.md b/operations/agent-enrollment.md index d48a2a3..a615c00 100644 --- a/operations/agent-enrollment.md +++ b/operations/agent-enrollment.md @@ -20,7 +20,7 @@ operational reference is here. | OS is Linux or macOS | bootstrap detects via `uname -s` | `uname -s` | | On the mesh (Netbird or Tailscale) **or** on the LAN | issuance is gated to mesh + LAN subnets. **For Netbird: use a setup-key, not interactive auth** — see "Getting onto Netbird" below. | `netbird status` / `tailscale status` | | `git`, `python3`, `python3-yaml`, `age`, `sops` | bootstrap preflight; `homelab` CLI imports yaml | See per-OS commands below | -| Can resolve `*.hubris.network` | bootstrap calls `https://secrets.hubris.network/issue` and writes `https://mcp.hubris.network` | `dig +short mcp.hubris.network` (should return `192.168.8.175`) | +| Can resolve `*.hubris.network` | bootstrap calls `https://secrets.hubris.network/issue` and writes `https://mcp.hubris.network/mcp` | `dig +short mcp.hubris.network` (should return `192.168.8.175`) | ### Hostname mismatch is the most common bootstrap failure @@ -83,7 +83,7 @@ If DNS isn't an option at all, override the URLs at bootstrap time: sudo HOMELAB_GITEA_TOKEN=... \ HOMELAB_REPO_URL=http://192.168.8.121:3000/dtoro/Homelab-Docs.git \ HOMELAB_ISSUANCE_NETBIRD=http://192.168.8.205:9820/issue \ - HOMELAB_MCP_URL=http://192.168.8.205:9810 \\ + HOMELAB_MCP_URL=http://192.168.8.205:9810/mcp \ bash /tmp/bootstrap.sh --with-mcp ``` diff --git a/tools/post-pull.sh b/tools/post-pull.sh index 8eaccc5..e913557 100644 --- a/tools/post-pull.sh +++ b/tools/post-pull.sh @@ -12,7 +12,8 @@ CONTEXT_DIR="${HOMELAB_CONTEXT_DIR:-/opt/homelab-context}" # Git safe.directory workaround: the repo is owned by the regular user # but launchd runs as root. Run git as the repo owner. -REPO_OWNER=$(stat -f "%Su" "$CONTEXT_DIR" 2>/dev/null || echo "root") +REPO_OWNER=$(stat -f "%Su" "$(readlink -f "$CONTEXT_DIR")" 2>/dev/null || \ + stat -f "%Su" "/Users/dtoro/Homelab-Docs" 2>/dev/null || echo "root") # 1. Git pull (as repo owner to avoid safe.directory issues) if [ "$REPO_OWNER" != "root" ] && command -v sudo &>/dev/null; then From 3154b3ca02ed0d9aad6eee89f3ea3a06e23c1f14 Mon Sep 17 00:00:00 2001 From: dtoro <d.toro.v@pm.me> Date: Tue, 2 Jun 2026 00:30:06 +0200 Subject: [PATCH 20/25] docs: mark MCP streamable-http migration resolved (#4) --- investigations/2026-06-01-mac-mini-onboarding.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/investigations/2026-06-01-mac-mini-onboarding.md b/investigations/2026-06-01-mac-mini-onboarding.md index 99eb1ce..a7eb631 100644 --- a/investigations/2026-06-01-mac-mini-onboarding.md +++ b/investigations/2026-06-01-mac-mini-onboarding.md @@ -183,3 +183,9 @@ Several layers of issues with git authentication: Captured the full onboarding of mac-mini into the hubris homelab context system, including the hostname mismatch, sudo/sops credential chain, model selection, git credential gymnastics, and editor clipboard interference. + +### 2026-06-02 — MCP server migrated to streamable-http +Item #4 resolved. FastMCP server now uses `transport="streamable-http"` (hyphen, +not underscore), serving at `https://mcp.hubris.network/mcp`. All doc references +and client configs (Goose, bootstrap.sh, agent-enrollment.md) updated. Service +re-deployed on apps LXC (105). From c7f68a095b20768324fb14449c4271474f23697c Mon Sep 17 00:00:00 2001 From: dtoro <d.toro.v@pm.me> Date: Tue, 2 Jun 2026 00:31:48 +0200 Subject: [PATCH 21/25] tools: post-pull auto-setup hook + caveman (RTK-style token optimization) --- AGENTS.md | 19 ++++++++++++++++++- operations/hermes-agent.md | 13 ++++++++++++- .../sync/linux/homelab-context-sync.service | 7 +++---- .../network.hubris.homelab-context-sync.plist | 11 ++++------- tools/post-pull.sh | 12 +++++++++--- 5 files changed, 46 insertions(+), 16 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 31888bf..f4b0e63 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -83,7 +83,24 @@ Grep is fine for browsing or when MCP is unreachable. - **Wiki updates**: same-session rule applies to any meaningful state change this client makes. -## 6. When in doubt +## 6. Auto-setup mechanism + +The homelab-context repo ships tooling that gets automatically installed +on every client after `git pull`. This is handled by `tools/post-pull.sh` +(replaces the raw git pull in the sync timer) which runs any script matching +`tools/*.setup.sh` after pull. + +Currently auto-setup: +- **Caveman + templates** (`tools/setup-caveman.sh`): Installs Caveman npm + package, wrapper scripts, and compact output templates for token-efficient + CLI output. Wrapper at `~/bin/caveman_wrapper.sh`. + +To add a new auto-setup, create `tools/<name>.setup.sh` in the repo, +commit and push. All enrolled clients pick it up within 5 minutes. + +To trigger sync manually: `sudo homelab sync` or wait for the 5-min timer. + +## 7. When in doubt Run `homelab mcp search_docs <query>` or `homelab mcp get_host <name>`. The clone is the fallback; MCP is the index. diff --git a/operations/hermes-agent.md b/operations/hermes-agent.md index 12b7cb7..2f87838 100644 --- a/operations/hermes-agent.md +++ b/operations/hermes-agent.md @@ -190,9 +190,20 @@ every tool call, use `approve`. See node. The wrapper, persona, and MCP wiring stay unchanged; only `GOOSE_PROVIDER`/`GOOSE_MODEL` change. +7. **Caveman auto-setup via post-pull hook.** The sync timer now calls + `tools/post-pull.sh`, which runs any `tools/*.setup.sh` after git pull. + Currently this auto-installs the Caveman npm package, wrapper scripts, and + compact output templates on all agent hosts (*token efficiency*). + ## Changelog -### 2026-05-31 — initial page +### 2026-06-01 — caveman + post-pull auto-setup +Added `tools/post-pull.sh` sync hook that auto-runs `tools/*.setup.sh` +after every git pull. First user: `tools/setup-caveman.sh` installed Caveman +templating + `~/bin/caveman_wrapper.sh` + `~/templates/*.txt` for token- +efficient CLI output. Replaces raw `git pull` in launchd/systemd timers. +Also created `tools/caveman/` with the wrapper script, JS renderer, and +templates — the canonical source for all agent hosts. Captures the Hermes-on-Goose onboarding flow added in the same commit as `bootstrap.sh --with-hermes`, `bin/hermes`, the sops rule for `secrets/openrouter-api-key.yaml`, and the `homelab client add --with-hermes` diff --git a/scripts/sync/linux/homelab-context-sync.service b/scripts/sync/linux/homelab-context-sync.service index adc650b..5ee58ab 100644 --- a/scripts/sync/linux/homelab-context-sync.service +++ b/scripts/sync/linux/homelab-context-sync.service @@ -1,15 +1,14 @@ [Unit] -Description=Pull /opt/homelab-context from Gitea +Description=Pull /opt/homelab-context from Gitea + auto-setup tooling After=network-online.target Wants=network-online.target [Service] Type=oneshot -ExecStart=/usr/bin/git -C /opt/homelab-context pull --ff-only --quiet +ExecStart=/usr/bin/env bash /opt/homelab-context/tools/post-pull.sh TimeoutStartSec=60 -# Don't fail aggressively — a missed pull just retries next tick. SuccessExitStatus=0 1 Nice=10 [Install] -WantedBy=multi-user.target +WantedBy=multi-user.target \ No newline at end of file diff --git a/scripts/sync/macos/network.hubris.homelab-context-sync.plist b/scripts/sync/macos/network.hubris.homelab-context-sync.plist index 814dc6e..a95dbc1 100644 --- a/scripts/sync/macos/network.hubris.homelab-context-sync.plist +++ b/scripts/sync/macos/network.hubris.homelab-context-sync.plist @@ -7,12 +7,9 @@ <key>ProgramArguments</key> <array> - <string>/usr/bin/git</string> - <string>-C</string> - <string>/opt/homelab-context</string> - <string>pull</string> - <string>--ff-only</string> - <string>--quiet</string> + <string>/usr/bin/env</string> + <string>bash</string> + <string>/opt/homelab-context/tools/post-pull.sh</string> </array> <key>StartInterval</key> @@ -29,4 +26,4 @@ <key>Nice</key> <integer>10</integer> </dict> -</plist> +</plist> \ No newline at end of file diff --git a/tools/post-pull.sh b/tools/post-pull.sh index e913557..ccbc3e6 100644 --- a/tools/post-pull.sh +++ b/tools/post-pull.sh @@ -12,12 +12,18 @@ CONTEXT_DIR="${HOMELAB_CONTEXT_DIR:-/opt/homelab-context}" # Git safe.directory workaround: the repo is owned by the regular user # but launchd runs as root. Run git as the repo owner. -REPO_OWNER=$(stat -f "%Su" "$(readlink -f "$CONTEXT_DIR")" 2>/dev/null || \ - stat -f "%Su" "/Users/dtoro/Homelab-Docs" 2>/dev/null || echo "root") +if command -v realpath &>/dev/null; then + REPO_REAL=$(realpath "$CONTEXT_DIR") +elif command -v grealpath &>/dev/null; then + REPO_REAL=$(grealpath "$CONTEXT_DIR") +else + REPO_REAL=$(perl -e 'print Cwd::abs_path(shift)' "$CONTEXT_DIR" 2>/dev/null || echo "/Users/dtoro/Homelab-Docs") +fi +REPO_OWNER=$(stat -f "%Su" "$REPO_REAL" 2>/dev/null || echo "dtoro") # 1. Git pull (as repo owner to avoid safe.directory issues) if [ "$REPO_OWNER" != "root" ] && command -v sudo &>/dev/null; then - sudo -u "$REPO_OWNER" git -C "$CONTEXT_DIR" pull --ff-only --quiet 2>&1 || \ + sudo -u "$REPO_OWNER" git -C "$CONTEXT_DIR" pull --ff-only --quiet 2>&1 | grep -v "failed to store" || \ echo "[post-pull] git pull failed (network?) continuing..." else git -C "$CONTEXT_DIR" pull --ff-only --quiet 2>&1 || \ From 1e7c5a7153cc4295deeb6409e0c5e3cd63c0635d Mon Sep 17 00:00:00 2001 From: dtoro <d.toro.v@pm.me> Date: Tue, 2 Jun 2026 00:54:03 +0200 Subject: [PATCH 22/25] hermes-soul: auto-provision SOUL.md from HERMES.md via setup-hermes-soul.sh - tools/setup-hermes-soul.sh: new auto-setup script (no-op on non-Hermes) - HERMES.md: canonical persona for all agent types, agent-type table - AGENTS.md: auto-setup section lists both caveman and hermes-soul - post-pull.sh runs it automatically on every sync --- AGENTS.md | 4 +++ HERMES.md | 47 ++++++++++++++++++++++---- tools/setup-hermes-soul.sh | 68 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 7 deletions(-) create mode 100755 tools/setup-hermes-soul.sh diff --git a/AGENTS.md b/AGENTS.md index f4b0e63..94c1ad0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -94,6 +94,10 @@ Currently auto-setup: - **Caveman + templates** (`tools/setup-caveman.sh`): Installs Caveman npm package, wrapper scripts, and compact output templates for token-efficient CLI output. Wrapper at `~/bin/caveman_wrapper.sh`. +- **Hermes agent persona** (`tools/setup-hermes-soul.sh`): Provisions + `~/.hermes/SOUL.md` from `HERMES.md` on Hermes agents. This ensures every + Hermes agent follows the canonical homelab persona (token efficiency, source + of truth hierarchy). No-op on non-Hermes agents. To add a new auto-setup, create `tools/<name>.setup.sh` in the repo, commit and push. All enrolled clients pick it up within 5 minutes. diff --git a/HERMES.md b/HERMES.md index 3be27b0..bb5cbf2 100644 --- a/HERMES.md +++ b/HERMES.md @@ -1,7 +1,33 @@ -# HERMES.md — Agent persona for Goose sessions on homelab clients +# HERMES.md — Agent persona for homelab clients -You are running on a machine in the **hubris** homelab. Your primary context -is `/opt/homelab-context/AGENTS.md` — read that first. +This file is the canonical agent persona for **all** AI agents running on +machines in the **hubris** homelab. It prescribes behaviour, token-efficiency +conventions, and the source-of-truth hierarchy. + +## Source of truth + +The homelab-context repo at `/opt/homelab-context/` is the single source of +truth for: +- Fleet topology (`inventory.yaml`, `hosts/*.yaml`) +- Service endpoints and credentials (via `homelab secret`) +- Agent behaviour and conventions +- Everything in this file + +When in doubt, check `/opt/homelab-context/` first. + +## Agent type — how this file gets loaded + +| Agent | Loading mechanism | +|-------|------------------| +| **Hermes** | `tools/setup-hermes-soul.sh` (auto-setup) → provisions `~/.hermes/SOUL.md` from this file | +| **Goose** | `.goosehints` symlink at `~/.config/goose/.goosehints` → `/opt/homelab-context/HERMES.md` | +| **Claude Code / Codex** | Symlink or copy this file into the project's `CLAUDES.md` / `.claude` instructions | + +**Do not edit SOUL.md or .goosehints directly.** Edit this file in the +homelab-context repo instead. Changes propagate to all clients on the next +sync (`sudo homelab sync`). + +--- ## Token efficiency (caveman skill) @@ -31,8 +57,8 @@ https://github.com/adityahimaone/hermes-agent-rtk-caveman. ### Templates Caveman templates live at `~/templates/`: -- `git_status.txt` — compact status format -- `git_log.txt` — compact log format +- `git_status.txt` — compact git status format +- `git_log.txt` — compact git log format - `lint_results.txt` — compact ESLint format - `test_results.txt` — compact vitest/jest format @@ -42,9 +68,16 @@ Caveman templates live at `~/templates/`: - Commands with no output — skip entirely - When you need the exact raw output for post-processing -## Verification +### Verification ```bash -# Check caveman is installed ls ~/bin/caveman_wrapper.sh && echo "caveman ready" ``` + +## Important note for Hermes agents + +If you are reading this as a Hermes agent, your SOUL.md was auto-provisioned +by `tools/setup-hermes-soul.sh`. This file is the canonical original — you +can verify the content matches or re-provision by running: + + bash /opt/homelab-context/tools/setup-hermes-soul.sh \ No newline at end of file diff --git a/tools/setup-hermes-soul.sh b/tools/setup-hermes-soul.sh new file mode 100755 index 0000000..ab2eb44 --- /dev/null +++ b/tools/setup-hermes-soul.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# setup-hermes-soul.sh — auto-provisions Hermes SOUL.md from canonical HERMES.md. +# Runs automatically after every homelab-context git pull (via tools/post-pull.sh). +# +# What it does: +# - Detects if Hermes Agent is installed (~/.hermes/SOUL.md exists) +# - If yes, copies the canonical HERMES.md content into SOUL.md with +# an auto-generated header that declares /opt/homelab-context as source of truth +# - Idempotent — re-running re-copies if HERMES.md content changed +# +# For non-Hermes agents (Goose, Claude Code, etc.), this script is a no-op. +# Those agents use the `.goosehints` symlink mechanism instead. + +set -euo pipefail + +CONTEXT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +HERMES_MD="$CONTEXT_DIR/HERMES.md" +SOUL_MD="${HOME}/.hermes/SOUL.md" + +# Colors for output (only when connected to a terminal) +if [ -t 1 ]; then + GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m' +else + GREEN=''; YELLOW=''; NC='' +fi +ok() { echo -e "${GREEN}[hermes-soul]${NC} $1"; } +skip() { echo -e "${YELLOW}[hermes-soul]${NC} $1"; } + +# --- 1. Check if Hermes is installed --- +if [ ! -f "$SOUL_MD" ]; then + skip "hermes not installed (~/.hermes/SOUL.md not found) — skipping" + exit 0 +fi + +# --- 2. Check if canonical HERMES.md exists --- +if [ ! -f "$HERMES_MD" ]; then + echo "[hermes-soul] WARNING: $HERMES_MD not found — skipping" + exit 0 +fi + +# --- 3. Write SOUL.md with canon source header + HERMES.md content --- +{ + echo "# Hermes Agent Persona — homelab agent (${HOSTNAME:-$(hostname -s 2>/dev/null || echo 'unknown')})" + echo "" + echo "You are an AI agent running in the **hubris** homelab." + echo "" + cat << 'PRE' +## Source of truth + +The homelab-context repo at `/opt/homelab-context/` is the single source of truth for: +- Fleet topology (`inventory.yaml`, `hosts/*.yaml`) +- Service endpoints and credentials +- Agent behaviour and conventions + +This SOUL.md is auto-generated from `/opt/homelab-context/HERMES.md` by +`tools/setup-hermes-soul.sh`. Do not edit SOUL.md directly — edit HERMES.md +in the homelab-context repo instead. Changes propagate automatically on the +next sync or by running: + + sudo homelab sync + +--- + +PRE + cat "$HERMES_MD" +} > "$SOUL_MD" + +ok "SOUL.md provisioned from HERMES.md ($(wc -l < "$SOUL_MD") lines)" \ No newline at end of file From 99d412370b82fe7960918b840123a6c947db0a3f Mon Sep 17 00:00:00 2001 From: dtoro <d.toro.v@pm.me> Date: Tue, 2 Jun 2026 21:34:56 +0200 Subject: [PATCH 23/25] =?UTF-8?q?network:=20migration=20complete=20?= =?UTF-8?q?=E2=80=94=20Proxmox=20as=20subnet=20router,=20Technitium=20DHCP?= =?UTF-8?q?=20live?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - plans/: mark Slate AX → SODOLA migration as Done (2026-06-02) - infrastructure/network.md: rewrite to reflect actual topology — Fritz!OS 8.x lacks second-IP-network support, so Proxmox routes between vmbr1 (192.168.178.10, uplink) and vmbr0 (192.168.8.x, internal portless bridge with 192.168.8.1 alias). Static route on Fritz!Box, no NAT on Proxmox. Technitium DHCP enabled for 192.168.8.100–240 (gateway .1, DNS .2). Caddy service unit was missing on CT 121 — recreated and enabled. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- infrastructure/network.md | 73 +++++++++++-------- ...2026-06-01-slate-ax-to-sodola-migration.md | 2 +- plans/index.md | 2 +- 3 files changed, 45 insertions(+), 32 deletions(-) diff --git a/infrastructure/network.md b/infrastructure/network.md index f94fbff..44cbab8 100644 --- a/infrastructure/network.md +++ b/infrastructure/network.md @@ -4,53 +4,63 @@ Physical and logical network topology for the homelab. ## Why -The homelab runs on a dedicated subnet (`192.168.8.0/24`) isolated from the main household LAN (`192.168.178.0/24`). Isolation is enforced at the Fritz!Box level: homelab devices can reach the internet and main LAN can reach homelab services via explicit port forwards, but main LAN cannot initiate arbitrary connections into the homelab subnet. +The homelab runs on a dedicated internal subnet (`192.168.8.0/24`) isolated from the main household LAN (`192.168.178.0/24`). Isolation is enforced at Proxmox: LXC/VM traffic is bridged only on the internal `vmbr0` bridge; Proxmox routes packets out to Fritz!Box via `vmbr1`. The main LAN cannot reach homelab services directly without a Fritz!Box static route (which is configured to allow inbound). + +Fritz!OS 8.x does not support second IP networks on LAN ports, so Proxmox (`hubris`) acts as the subnet router rather than the Fritz!Box. ## Hardware | Device | Role | |---|---| -| Fritz!Box 7590 | Main router / ISP gateway, VLAN router between subnets | -| SODOLA 5-Port 2.5Gbit Managed | Homelab switch, all ports in VLAN 10 | - -## VLANs - -| VLAN | Subnet | Gateway | Devices | -|---|---|---|---| -| 1 (native) | 192.168.178.0/24 | 192.168.178.1 | Household LAN — laptops, phones, APs | -| 10 | 192.168.8.0/24 | 192.168.8.1 | Homelab — Proxmox, all LXCs/VMs | +| Fritz!Box 7590 | Main router / ISP gateway (`192.168.178.1`) | +| SODOLA 5-Port 2.5Gbit Managed | Homelab switch — flat L2, all ports native | +| hubris (Proxmox) | Subnet router — routes between `192.168.8.0/24` and `192.168.178.0/24` | ## Topology ``` ISP - └── Fritz!Box 7590 - ├── VLAN 1 192.168.178.0/24 (main LAN) - └── VLAN 10 192.168.8.0/24 (homelab) - │ - └── SODOLA 5-Port 2.5Gbit - ├── Port 1 uplink → Fritz!Box - ├── Port 2 hubris (Proxmox, 192.168.8.77) - ├── Port 3 [device] - ├── Port 4 [device] - └── Port 5 spare + └── Fritz!Box 7590 (192.168.178.1) + │ static route: 192.168.8.0/24 → 192.168.178.10 + │ + └── SODOLA 5-Port 2.5Gbit + ├── Port 1 uplink → Fritz!Box LAN + ├── Port 2 hubris eno1 → vmbr1 (192.168.178.10) + ├── Port 3 [device] + ├── Port 4 [device] + └── Port 5 spare + +hubris internal bridges: + vmbr1 192.168.178.10/24 eno1 (uplink, DHCP-reserved) gateway 192.168.178.1 + vmbr0 192.168.8.77/24 no physical port (internal) + 192.168.8.1/24 alias — LXC default gateway + ├── all 16 LXCs + └── HAOS VM ``` +## Subnets + +| Subnet | Gateway | Purpose | +|---|---|---| +| `192.168.178.0/24` | `192.168.178.1` | Household LAN — laptops, phones, Fritz!Box DHCP | +| `192.168.8.0/24` | `192.168.8.1` (Proxmox `vmbr0` alias) | Homelab — all LXCs and VMs | + ## DHCP -Fritz!Box serves DHCP for both VLANs. For VLAN 10 homelab: -- Range: `192.168.8.100 – 192.168.8.240` -- DNS: `192.168.8.2` (Technitium on [CT 107](../containers/107-dns.md)) +- **Household (`192.168.178.x`)**: Fritz!Box built-in DHCP. Proxmox `vmbr1` has a reservation: MAC `84:47:09:6b:e7:58` → `192.168.178.10`. +- **Homelab (`192.168.8.x`)**: Technitium on [CT 107](../containers/107-dns.md) at `192.168.8.2`. Range `192.168.8.100–192.168.8.240`, gateway `192.168.8.1`, DNS `192.168.8.2`. -Most homelab hosts use static IPs below `.100` — DHCP only assigns to new/transient devices. +Most homelab LXCs use static IPs below `.100`. DHCP only covers new/transient containers. ## DNS Split-horizon DNS for `*.hubris.network` served by Technitium on [CT 107](../containers/107-dns.md) at `192.168.8.2:53`. See [dns.md](dns.md) for full detail. -## Port forwarding +## Routing -All inbound port forwards are on the Fritz!Box directly. The old double-hop (Fritz!Box → Slate AX → homelab) was eliminated when the Slate AX was retired. Target IPs are homelab addresses (`192.168.8.x`). +Proxmox has `net.ipv4.ip_forward=1` (already enabled by PVE). Packets from LXCs on `vmbr0` destined for the internet exit via `vmbr1` → Fritz!Box. Fritz!Box masquerades all outbound WAN traffic. Fritz!Box has a static route (`192.168.8.0/24 → 192.168.178.10`) so return traffic reaches the LXCs. + +No NAT on Proxmox — traffic flows without double-NAT. ## Remote access @@ -62,10 +72,13 @@ All inbound port forwards are on the Fritz!Box directly. The old double-hop (Fri - [DNS](dns.md) — split-horizon config and entry list - [Ingress](ingress.md) — public entry points via VPS traefik - [Mesh](mesh.md) — NetBird / Tailscale VPN overlay -- [hosts/hubris.md](../hosts/hubris.md) — Proxmox host -- [CT 107 — dns](../containers/107-dns.md) — Technitium DNS server +- [hosts/hubris.md](../hosts/hubris.md) — Proxmox host (vmbr0/vmbr1 config) +- [CT 107 — dns](../containers/107-dns.md) — Technitium DNS + DHCP server ## Changelog -### 2026-06-01 — Slate AX retired; SODOLA switch added; Fritz!Box becomes homelab gateway -Replaced the GL.iNet Slate AX sub-router with the SODOLA 5-Port 2.5Gbit managed switch. Fritz!Box 7590 now routes VLAN 10 (`192.168.8.0/24`) directly — no more double-NAT. Gateway `192.168.8.1` is unchanged so no LXC or VM needed reconfiguration. Fritz!Box DHCP for VLAN 10 now hands out `192.168.8.2` (Technitium) as DNS, resolving the outstanding "update router DHCP DNS" item. See [migration plan](../plans/2026-06-01-slate-ax-to-sodola-migration.md). +### 2026-06-02 — Executed migration; Proxmox as subnet router +Fritz!OS 8.x does not support second IP networks on LAN ports, so the final design uses Proxmox as the router: `vmbr1` (eno1 → SODOLA → Fritz!Box) is the uplink at `192.168.178.10`; `vmbr0` is a portless internal bridge with `192.168.8.1` alias as the LXC gateway. Technitium DHCP enabled for `192.168.8.100–240`. Caddy service unit was missing and recreated. See [migration plan](../plans/2026-06-01-slate-ax-to-sodola-migration.md). + +### 2026-06-01 — Initial network doc; Slate AX retired; SODOLA switch added +Replaced the GL.iNet Slate AX sub-router with the SODOLA 5-Port 2.5Gbit managed switch. Eliminated double-NAT. See [migration plan](../plans/2026-06-01-slate-ax-to-sodola-migration.md). diff --git a/plans/2026-06-01-slate-ax-to-sodola-migration.md b/plans/2026-06-01-slate-ax-to-sodola-migration.md index 90b8c83..8f67eae 100644 --- a/plans/2026-06-01-slate-ax-to-sodola-migration.md +++ b/plans/2026-06-01-slate-ax-to-sodola-migration.md @@ -1,6 +1,6 @@ # 2026-06-01 — Slate AX → SODOLA Migration -**Status:** Planned +**Status:** Done — 2026-06-02 **Hardware:** SODOLA 5-Port 2.5Gbit Managed Switch replacing GL.iNet Slate AX **Router:** Fritz!Box 7590 diff --git a/plans/index.md b/plans/index.md index 6268434..9aab22c 100644 --- a/plans/index.md +++ b/plans/index.md @@ -6,7 +6,7 @@ Pre-flight runbooks for planned changes that haven't happened yet. Once executed | Date | Title | Status | | ---- | ----- | ------ | -| 2026-06-01 | [Slate AX → SODOLA managed switch migration](2026-06-01-slate-ax-to-sodola-migration.md) | Planned | +| 2026-06-01 | [Slate AX → SODOLA managed switch migration](2026-06-01-slate-ax-to-sodola-migration.md) | Done | ## Conventions From 41de39524079ea4dbb6656d757143f205c667ff2 Mon Sep 17 00:00:00 2001 From: dtoro <d.toro.v@pm.me> Date: Tue, 2 Jun 2026 22:02:34 +0200 Subject: [PATCH 24/25] docs: post-migration changelog entries on hubris, caddy, dns hosts/hubris.md: - Update At a glance network section: vmbr1 uplink (192.168.178.10), vmbr0 portless internal bridge with 192.168.8.1 alias - Remove Phase 1 WiFi failover section (wlp3s0 disabled 2026-06-02) - Changelog: Slate AX retired, SODOLA added, Proxmox as subnet router containers/121-caddy.md: - Changelog: caddy.service unit was missing from hubris1 package, recreated manually; risk of loss on package reinstall noted containers/107-dns.md: - Update Who points here: Technitium DHCP hands out .2 as DNS for homelab clients; Fritz!Box LAN clients still get Fritz!Box DNS - Add DHCP section documenting the homelab scope (100-240, gw .1) - Changelog: DHCP enabled 2026-06-02, replaces Slate AX DHCP Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- containers/107-dns.md | 16 +++++++++++++++- containers/121-caddy.md | 3 +++ hosts/hubris.md | 19 ++++++------------- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/containers/107-dns.md b/containers/107-dns.md index 2dbee90..1603feb 100644 --- a/containers/107-dns.md +++ b/containers/107-dns.md @@ -25,9 +25,20 @@ Authoritative split-horizon DNS for `hubris.network` on the LAN/mesh, plus recur ## Who points here - **NetBird mesh peers:** nameserver group `home-lab-dns` (`hubris.network` → this server). *Note: the [NetBird managed DNS zone](124-authentik.md) still answers most app names directly, bypassing this — see the single-source-of-truth decision.* -- **Plain LAN clients:** router DHCP should hand out `192.168.8.2` (update the router — the old `.180`/dnsmasq is gone). +- **Homelab DHCP clients:** Technitium's own DHCP scope hands out `192.168.8.2` as the DNS server for `192.168.8.x` leases (see DHCP section below). +- **Plain LAN clients (`192.168.178.x`):** Fritz!Box DHCP still hands out Fritz!Box itself (`192.168.178.1`) as DNS — no split-horizon for non-mesh clients. Changing this requires a secondary DNS fallback, which Fritz!OS 8.x doesn't expose in a single DHCP field. - **Secondary:** Mac Mini (planned, Phase 3) via AXFR zone transfer. +## DHCP + +Technitium also runs a DHCP server for the homelab subnet (enabled 2026-06-02): +- **Scope:** `homelab` — `192.168.8.100 – 192.168.8.240` +- **Gateway:** `192.168.8.1` (Proxmox `vmbr0` alias) +- **DNS:** `192.168.8.2` (self) +- **Lease time:** 24 h + +Replaces the DHCP that was previously served by the Slate AX router. Static-IP LXCs (below `.100`) are unaffected. + ## Related - [124 — authentik](124-authentik.md) — retired host of the old dnsmasq - [DNS split-horizon](../infrastructure/dns.md) @@ -35,5 +46,8 @@ Authoritative split-horizon DNS for `hubris.network` on the LAN/mesh, plus recur ## Changelog +### 2026-06-02 — DHCP server enabled; replaces Slate AX DHCP +Enabled Technitium's built-in DHCP server for `192.168.8.0/24` (scope `homelab`, range `.100–.240`, gateway `192.168.8.1`, DNS self). Previously the Slate AX sub-router served DHCP for the homelab subnet. With the Slate AX retired and Proxmox now the subnet router, Technitium takes over DHCP. Configured via the Technitium API (`/api/dhcp/scopes/set`). DHCP LXCs kept their Slate AX leases until expiry, then renewed from Technitium. + ### 2026-06-01 — created; replaced dnsmasq on 124 Stood up Technitium at `192.168.8.2`, imported the split-horizon zone (specific A + wildcard + MX/SPF/CAA), made it the primary nameserver in the NetBird `home-lab-dns` group. Verified all names resolve with dnsmasq/124 stopped; [LXC 124 retired](124-authentik.md). diff --git a/containers/121-caddy.md b/containers/121-caddy.md index bda0433..ceda2eb 100644 --- a/containers/121-caddy.md +++ b/containers/121-caddy.md @@ -65,6 +65,9 @@ Gitea webhook id 2 on `dtoro/caddy-conf`. Receiver, deploy script, install scrip ## Changelog +### 2026-06-02 — caddy.service unit missing; recreated +After the Slate AX → SODOLA network migration, Caddy was not listening (ports 80/443 dead). Root cause: the custom hubris1 Debian package (`caddy_1:2.11.3-hubris1_amd64`) does not ship a systemd service unit file. The unit had previously existed but was lost (likely on a package reinstall). Recreated at `/lib/systemd/system/caddy.service` with standard Caddy service config + `EnvironmentFile=/etc/caddy/caddy.env` (already present in `caddy.service.d/override.conf`). **Risk:** the unit will be lost again if the package is reinstalled without the file being tracked. Fix: add the service unit to the `caddy-conf` repo or rebuild the hubris1 package to include it. + ### 2026-04-28 — wiki entry created Initial documentation. 16 active sites at this date. diff --git a/hosts/hubris.md b/hosts/hubris.md index ec259e8..4bde448 100644 --- a/hosts/hubris.md +++ b/hosts/hubris.md @@ -6,8 +6,9 @@ Single-node Proxmox VE running 1 VM and 13 LXC containers. The whole homelab. - **Role:** Proxmox VE 9.1.2 hypervisor (kernel `6.14.11-4-pve`) - **Hardware:** GMKtec NucBox M6 Ultra — AMD Ryzen 5 7640HS (Phoenix APU), 12 vCPU / ~28 GiB RAM, 2× Samsung 990 EVO Plus NVMe (one SSD primary, one for `library` LVM). 2× Realtek RTL8125 NICs (`r8169`). - **BIOS:** 1.02 (2025-08-06) — vendor not on LVFS, no automated update path. See [investigations](../investigations/2026-04-21-hubris-crash-loop.md). -- **LAN (primary):** `192.168.8.77/24` on bridge `vmbr0` (slave: `eno1`), gateway `192.168.8.1`. Default route metric 0. -- **WiFi (failover):** `192.168.8.141/24` on `wlp3s0` (MediaTek MT7922, AX), DHCP from the same router. Default route metric 200. See [Phase 1 WiFi failover](#phase-1-wifi-failover) below. +- **Uplink:** `vmbr1` (slave: `eno1`) → SODOLA switch → Fritz!Box 7590. DHCP-reserved `192.168.178.10/24`, gateway `192.168.178.1`. +- **Homelab bridge:** `vmbr0` — portless internal bridge, `192.168.8.77/24` + `192.168.8.1/24` alias (LXC default gateway). All 16 LXCs and the HAOS VM are on `vmbr0`. Proxmox routes between `vmbr0` and `vmbr1`; Fritz!Box has a static route `192.168.8.0/24 → 192.168.178.10`. +- **WiFi:** disabled 2026-06-02 — `wlp3s0` removed from `/etc/network/interfaces`, wpa config deleted. Was used as a failover to the now-retired Slate AX AP. - **Mesh:** Netbird `wt0` `100.122.38.109/16`. Resolver: `100.122.38.109` (the local netbird daemon, which forwards to LAN/upstream and learns `*.hubris.network` answers via that path). See [mesh](../infrastructure/mesh.md). - **UI:** `https://proxmox.hubris.network` (via [caddy](../containers/121-caddy.md)) or `https://192.168.8.77:8006`. @@ -36,17 +37,6 @@ See [containers/index](../containers/index.md). 13 active (109 syncthing destroy - **Crash capture:** `/etc/sysctl.d/60-crash-capture.conf` (panic on oops/hardlockup/softlockup/rcu, auto-reboot 10 s), `/etc/modprobe.d/softdog.conf` (`soft_panic=1 soft_margin=60`), `/etc/systemd/system.conf.d/watchdog.conf` (`RuntimeWatchdogSec=15s`). Pstore traces collected to `/var/lib/systemd/pstore/` by `systemd-pstore.service`. **Caveat:** silent CPU lockups leave pstore empty. - **`rasdaemon`** (Debian pkg) collects MCE / memory / PCIe AER / thermal events to `/var/lib/rasdaemon/ras-mc_event.db`. Query with `ras-mc-ctl --summary` / `--errors`. (mcelog is retired in Debian 13 — don't go looking for it.) -## Phase 1 WiFi failover - -Host is dual-homed on LAN (`eno1`/`vmbr0`) and WiFi (`wlp3s0`) so management/SSH stay reachable when LAN drops. **Guests are not yet failed over** — the LXC fleet remains on `vmbr0`/`eno1`. Phase 2 will migrate guest networking off the bridge so the homelab survives full LAN loss. - -- WiFi creds in `/etc/wpa_supplicant/wpa_supplicant-wlp3s0.conf` (hashed PSK, mode 600). SSID lives in `/etc/network/interfaces` as `wpa-conf`. -- Both interfaces sit on the same `192.168.8.0/24`; cross-talk avoided with `arp_ignore=1` + `arp_announce=2` on `eno1`/`vmbr0`/`wlp3s0` (set via `post-up` in `/etc/network/interfaces`). -- A second default route at metric 200 is added on `wlp3s0` (post-up). LAN wins while up. -- **Carrier-based failover:** `vmbr0`'s carrier follows the LXC veth members, so it stays `1` even when `eno1` loses link. `ignore_routes_with_linkdown` is therefore not enough on its own. `wan-failover.service` (`/usr/local/sbin/wan-failover.sh`) watches `/sys/class/net/eno1/carrier` via `ip monitor link` and removes/restores the `vmbr0` default route on transitions. Logs to `journalctl -t wan-failover`. -- Failover verified 2026-04-28: `ip link set eno1 down` → outbound HTTP keeps working via WiFi; `ip link set eno1 up` → vmbr0 default restored. -- Reachable on `192.168.8.77` (LAN) and `192.168.8.141` (WiFi); SSH works on either. - ## Network performance baseline (2026-05-14) | Path | Throughput | Notes | @@ -99,6 +89,9 @@ OpenSSH on `0.0.0.0:22`. Netbird's built-in SSH server is on `100.122.38.109:220 ## Changelog +### 2026-06-02 — Slate AX retired; SODOLA switch added; network restructured +Replaced GL.iNet Slate AX sub-router with SODOLA 5-Port 2.5Gbit managed switch. Fritz!OS 8.x lacks second-IP-network support on LAN ports, so Proxmox now acts as the subnet router: `vmbr1` (eno1 → SODOLA → Fritz!Box) is the uplink at `192.168.178.10/24`; `vmbr0` is a portless internal bridge holding all LXCs/VMs with `192.168.8.1` as an alias (unchanged LXC gateway). Fritz!Box static route `192.168.8.0/24 → 192.168.178.10` enables inbound routing. No LXC configs changed. Eliminated double-NAT. WiFi (`wlp3s0`) also removed — was pointing at the Slate AX SSID, no longer useful. See [network](../infrastructure/network.md) and [migration plan](../plans/2026-06-01-slate-ax-to-sodola-migration.md). + ### 2026-05-14 — LXC 109 (syncthing) decommissioned User destroyed the syncthing LXC (had been stopped since 2026-04-21, never re-enabled). `pct destroy 109 --purge` cleaned `vm-109-disk-0` on `local-lvm` and the `/etc/pve/lxc/109.conf` entry. Data subtree `/mnt/library/syncthing` was already empty and retained as an empty dir. No DNS, Caddy, NFS-export, or claudio-monitor references to clean up. Entry moved to the "recently destroyed" table in [containers/index](../containers/index.md#recently-destroyed-kept-for-archaeology); references stripped from [README](../README.md), [media-permissions](../infrastructure/media-permissions.md), [vms/100-zimaos](../vms/100-zimaos.md), and [containers/102-nfs-export](../containers/102-nfs-export.md). From ce1bf4d0d229b6e76c04e848cb0bb01d76dd5d37 Mon Sep 17 00:00:00 2001 From: dtoro <d.toro.v@pm.me> Date: Tue, 2 Jun 2026 22:42:46 +0200 Subject: [PATCH 25/25] docs: gluetun VPN + static IP migration (2026-06-02) --- containers/122-arriman.md | 10 ++++++++++ infrastructure/dns.md | 3 +++ 2 files changed, 13 insertions(+) diff --git a/containers/122-arriman.md b/containers/122-arriman.md index 04f9542..7ad2583 100644 --- a/containers/122-arriman.md +++ b/containers/122-arriman.md @@ -101,6 +101,16 @@ Member of [media GID 10000](../infrastructure/media-permissions.md). The LXC has ## Changelog +### 2026-06-02 — ProtonVPN added (gluetun); LXC IP set static +- Added `gluetun` container to compose as a WireGuard VPN sidecar (ProtonVPN, server AL#57, located in Tirana, Albania) +- **qbittorrent** and **sabnzbd** now use `network_mode: service:gluetun` — all traffic routes through the VPN +- Ports 8080 (qBit WebUI), 6881 tcp/udp (qBit BT), 8081 (SAB WebUI) exposed through gluetun +- gluetun config at `gluetun-config/wireguard/wg0.conf` (read-only mount) +- Healthcheck on gluetun; qBit/SAB wait for `service_healthy` before starting +- LXC IP changed from DHCP to static (`192.168.8.132`) via `pct set` + `/etc/network/interfaces` +- **After first start:** Sonarr/Radarr/Lidarr download client host needs updating from `qbittorrent` → `gluetun` (SAB similarly `sabnzbd` → `gluetun`) +- **Also fixed:** 7 other DHCP LXCs (101 jellyfin, 103 paperless, 104 gitea, 105 apps, 114 nextcloud, 118 elementsynapse, 120 mule-images, 121 caddy) set to static IPs to prevent floating on reboot. See infrastructure/dns.md. + ### 2026-04-28 — wiki entry created Initial documentation. diff --git a/infrastructure/dns.md b/infrastructure/dns.md index fd58042..1beb949 100644 --- a/infrastructure/dns.md +++ b/infrastructure/dns.md @@ -80,6 +80,9 @@ Either: ## Changelog +### 2026-06-02 — 8 LXCs moved from DHCP to static IP +All LXCs that Caddy reverse-proxies to by IP were on `ip=dhcp` and could float on reboot (arriman got a different lease mid-session and broke). Fixed via `pct set` + in-LXC `/etc/network/interfaces`. Affected: 101 jellyfin, 103 paperless, 104 gitea, 105 apps, 114 nextcloud, 118 elementsynapse, 120 mule-images, 121 caddy, 122 arriman. See [arriman changelog](../containers/122-arriman.md#changelog). + ### 2026-06-01 — dnsmasq replaced by Technitium on [dns (107)](../containers/107-dns.md); LXC 124 retired Split-horizon DNS moved off [124](../containers/124-authentik.md) to a dedicated **Technitium** LXC at **`192.168.8.2`** (zone: specific A overrides + wildcard→VPS + replicated MX/SPF/CAA). NetBird `home-lab-dns` nameserver group cut over to `192.168.8.2` (with `.180` as a now-dead fallback). dnsmasq stopped, all names verified via Technitium, **LXC 124 shut down**. **Caveat:** the [NetBird managed DNS zone](../containers/124-authentik.md) still answers most app names *directly* (bypassing the nameserver group) — three overlapping DNS sources remain; see the single-source-of-truth decision (Phase 4). **Action needed:** update router DHCP DNS from the dead `.180` → `192.168.8.2` for any plain-LAN (non-mesh) clients.