# Nomos agent — LLM-powered terminal 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 the configured LLM - 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/NOMOS.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-nomos` | ## 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-nomos. TOKEN=... # gitea PAT, read:repository curl -fsSL -u "dtoro:$TOKEN" \ https://git.hubris.network/dtoro/oikos/raw/branch/main/bootstrap.sh \ -o /tmp/bootstrap.sh sudo HOMELAB_GITEA_TOKEN=$TOKEN bash /tmp/bootstrap.sh --with-mcp --with-nomos # 4. Back on hubris: finalize the age pubkey AND grant the Nomos secret. homelab client add new-machine \ --finalize-pubkey age1... \ --with-nomos # 5. Wait ≤5 min for sync, then on new-machine: nomos "what LXCs are running?" ``` The bootstrap `--with-nomos` 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/nomos` → `/usr/local/bin/nomos`. 3. Symlinks `/opt/homelab-context/NOMOS.md` → `/root/NOMOS.md` (Linux) or `/etc/NOMOS.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` → NOMOS.md, so the persona is injected as the system prompt on every session. ## Seeding the OpenRouter key The first time anyone enrolls with `--with-nomos`, 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, `nomos …` exits with `openrouter-api-key.yaml still contains the placeholder`. Subsequent enrollees get the real key automatically via `--with-nomos` (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-nomos` 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-nomos ``` `--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 nomos # binaries present goose info -v # provider/model wiring sane nomos "what LXCs are running?" # interactive Goose session # Non-interactive smoke test: echo "List the homelab MCP tools you have available" | nomos ``` ## 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_NOMOS_MODEL=nousresearch/hermes-3-llama-3.1-405b \ HOMELAB_NOMOS_MCP_URI=https://mcp.hubris.network/mcp \ sudo bash /tmp/bootstrap.sh --with-nomos ``` 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 | | --- | --- | --- | | `nomos: could not decrypt secrets/openrouter-api-key.yaml` | Host isn't a recipient on the sops rule | `homelab client add --finalize-pubkey --with-nomos` from hubris | | `nomos: 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 `nomos` 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 upgraded in Go rewrite (`internal/mcp/server.go`, Streamable HTTP via official MCP SDK). Old FastMCP SSE transport is deprecated. | Run `docker compose --profile full up` on mac-mini, or wait for the production cutover from apps/105. | | `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-nomos`; 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 Nomos flow assumes is done. - [`NOMOS.md`](../NOMOS.md) — the persona the Nomos agent reads on every session start (via `~/.config/goose/.goosehints`). - [`bin/nomos`](../../bin/nomos) — the wrapper that decrypts the OpenRouter key and execs `goose session`. - [`bootstrap.sh`](../../bootstrap.sh) — the `--with-nomos` 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 `internal/mcp/server.go` uses Streamable HTTP (official MCP SDK). 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 Nomos 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 Nomos-on-Goose onboarding flow added in the same commit as `bootstrap.sh --with-nomos`, `bin/nomos`, the sops rule for `secrets/openrouter-api-key.yaml`, and the `homelab client add --with-nomos` extension. MCP streamable_http migration is queued as follow-up #1.