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