Problem: "Hermes" collides with Nous Researchs unrelated product; unclear identity for the resident agent. Change: Rename the live service identity across 39 files: - cmd/hermes/ → cmd/nomos/ (binary, env vars NOMOS_*) - internal/config/ server.go (NomosAgentSlug, nomosAgentID) - compose/hermes/ → compose/nomos/ (Dockerfile, service name) - hermes/ → nomos/ (SOUL.md, config.yaml, skills/) - .agents/HERMES.md → NOMOS.md (persona) - tools/setup-hermes-soul.sh → setup-nomos-soul.sh - seeds/inventory.yaml (agent:hermes → agent:nomos) - migrations/014_rename_agent_hermes_to_nomos.up.sql - Caddy vhost hermes.hubris.network → nomos.hubris.network - All referencing docs, scripts, ADR notes History preserved: archive/, plans/done/, ADRs not rewritten. Matrix @hermes notifier account and Legacy bin/hermes on LXC 129 intentionally untouched (out of scope). Risk: N0 is identity-only rename; zero behavioral changes. Verification: go build ./... passes; docker compose --profile full resolves nomos service; grep -ri hermes (excluding archive/plans) returns only intentional refs (LLM model name, Matrix user).
211 lines
9.6 KiB
Markdown
211 lines
9.6 KiB
Markdown
# 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 <host> --finalize-pubkey <age1...> --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 <host> | grep age_pubkey | awk '{print $2}')
|
|
homelab client add <host> --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 <host> --finalize-pubkey <age1...> --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.
|