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).
9.6 KiB
Nomos agent — LLM-powered terminal sessions on a homelab client
Onboards Nous Research's Hermes (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) — this page covers only the Hermes-specific additions.
The agent runs as a Goose session. Goose provides:
- The chat loop, multi-turn history, and streaming
- The OpenRouter provider that routes to the configured LLM
- The built-in
developerextension (shell + file editor — same surface Claude Code has) - A remote MCP extension pointed at
mcp.hubris.networkfor 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 |
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
# 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:
- Downloads the latest Goose binary into the operator's
~/.local/bin/goose(upstream installer) and symlinks/usr/local/bin/gooseto it. - Symlinks
/opt/homelab-context/bin/nomos→/usr/local/bin/nomos. - Symlinks
/opt/homelab-context/NOMOS.md→/root/NOMOS.md(Linux) or/etc/NOMOS.md(macOS) forcat-as-operator convenience. - Drops
~/.config/goose/config.yamlpinning the provider, model, and extensions (preserves any keys the operator added by hand). - 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):
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:
# 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
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:
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:
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.
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 — base client onboarding the Nomos flow assumes is done.
NOMOS.md— the persona the Nomos agent reads on every session start (via~/.config/goose/.goosehints).bin/nomos— the wrapper that decrypts the OpenRouter key and execsgoose session.bootstrap.sh— the--with-nomosflag's install block.
Follow-ups
-
Migrate the MCP server to streamable_http. Goose 1.x deprecated SSE (
"SSE transport is no longer supported - kept only for config file compatibility"incrates/goose/src/agents/extension.rs). Our FastMCP server atinternal/mcp/server.gouses Streamable HTTP (official MCP SDK). Until that's changed, thehomelabMCP 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. -
Per-host OpenRouter keys for billing attribution. Today all Nomos hosts share one key.
-
Pin the model version rather than tracking
nousresearch/hermes-4-405bdirectly — OpenRouter periodically rotates the underlying weights. -
Local-inference fallback (ollama / vllm) once the homelab has a GPU node. The wrapper, persona, and MCP wiring stay unchanged; only
GOOSE_PROVIDER/GOOSE_MODELchange. -
Caveman auto-setup via post-pull hook. The sync timer now calls
tools/post-pull.sh, which runs anytools/*.setup.shafter 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.