Files
oikos/.agents/operations/nomos-agent.md
dtoro 2b3aa248b1 N0: rename Hermes → Nomos (standalone commit)
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).
2026-07-08 14:14:56 +02:00

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 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
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:

  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):

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 execs goose session.
  • 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.

  5. 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.