211 lines
9.7 KiB
Markdown
211 lines
9.7 KiB
Markdown
# 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 <host> --finalize-pubkey <age1...> --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 <host> | grep age_pubkey | awk '{print $2}')
|
|
homelab client add <host> --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: deepseek/deepseek-v4-flash
|
|
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 <host> --finalize-pubkey <age1...> --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.
|
|
|
|
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-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`
|
|
extension. MCP streamable_http migration is queued as follow-up #1.
|