hermes-agent: onboard Nous-Hermes-on-Goose to homelab clients
`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) <noreply@anthropic.com>
This commit is contained in:
115
bootstrap.sh
115
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 <<PYEOF
|
||||
import os, sys
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
print("PyYAML required", file=sys.stderr); sys.exit(2)
|
||||
|
||||
path = "$GOOSE_CONFIG"
|
||||
mcp_uri = "$HERMES_MCP_URI"
|
||||
model = "$HERMES_MODEL"
|
||||
|
||||
cfg = {}
|
||||
if os.path.exists(path):
|
||||
with open(path) as f:
|
||||
try:
|
||||
cfg = yaml.safe_load(f) or {}
|
||||
except Exception:
|
||||
cfg = {}
|
||||
|
||||
cfg["GOOSE_PROVIDER"] = "openrouter"
|
||||
cfg["GOOSE_MODEL"] = model
|
||||
cfg.setdefault("GOOSE_MODE", "smart_approve")
|
||||
cfg.setdefault("extensions", {})
|
||||
cfg["extensions"]["developer"] = {
|
||||
"bundled": True, "enabled": True, "name": "developer",
|
||||
"timeout": 300, "type": "builtin",
|
||||
}
|
||||
cfg["extensions"]["homelab"] = {
|
||||
"enabled": True, "name": "homelab",
|
||||
"description": "Read-only homelab context tools (FastMCP).",
|
||||
"type": "streamable_http", "uri": mcp_uri, "timeout": 60,
|
||||
}
|
||||
with open(path, "w") as f:
|
||||
yaml.safe_dump(cfg, f, sort_keys=False)
|
||||
print("[bootstrap] merged Goose config at", path)
|
||||
PYEOF
|
||||
)
|
||||
if [ "$DRY_RUN" -eq 1 ]; then
|
||||
echo "+ would merge Goose config at $GOOSE_CONFIG"
|
||||
else
|
||||
python3 -c "$PY_GOOSE_MERGE"
|
||||
chown -R "$H_USER" "$GOOSE_DIR"
|
||||
fi
|
||||
|
||||
# 5. Symlink HERMES.md as the global .goosehints — Goose injects it into
|
||||
# the system prompt on every session start.
|
||||
run "ln -sfn '$CLONE_DIR/HERMES.md' '$GOOSEHINTS'"
|
||||
if [ "$DRY_RUN" -eq 0 ]; then
|
||||
chown -h "$H_USER" "$GOOSEHINTS" 2>/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
|
||||
|
||||
Reference in New Issue
Block a user