Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions briefings/hermes-agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@
- **Upstream dashboard runs ~160 async handlers on one event loop with the SYNC httpx.Client (12 call sites, 0 AsyncClient)** β€” one blocking outbound call freezes every route (measured /login 0.00β†’7.78 s, freeze == timeout). Real risk on egress-restricted networks; fix is upstream.
- **Chromium is NOT in the image** β€” doctor.py fetches it via `npx playwright install chromium` on first browser use (needs egress). The old "baked in" claim was false.
- **`model.name` must be the bare hyphenated provider ID** (`claude-opus-4-6`); the chart prefixes `anthropic/` itself, unguarded β€” a pre-prefixed value renders `anthropic/anthropic/…`. The image's own default `anthropic/claude-opus-4.6` (dotted) is not in the provider's live model list.
- **Browser automation: NOT OPERABLE on v2026.8.31, proven by exhaustion (2026-09-02, six attempts).** The image ships THREE browser drivers and zero installed: default engine `lightpanda` (absent), `agent-browser` CLI (absent β€” and works perfectly once npm-installed: chromium downloads in 6 s to the image-pinned path, all system libs present, page fetches succeed by hand even under the gateway's scrubbed env), and the ACTUALLY-ACTIVE backend `browser_exec` β†’ the `browser-use` CLI (absent; uv-installable) which even then demands a running Chrome to attach. The failure is silent: agent falls back to `web_extract` and the model paraphrases the tool error as "no Chrome available". Also: the run-as-hermes gateway has EMPTY HOME and no PLAYWRIGHT_BROWSERS_PATH; `/opt/data/.local/bin` IS on its PATH (volume β€” the right home for any future boot-warm). Do not re-litigate from the README's old "downloads on demand" claim; that described the download half only. **Attempt 7 SUCCEEDED β€” CDP attach is the supportable path**: a persistent headless chromium (`--headless --no-sandbox --remote-debugging-port=N --user-data-dir=/opt/data/.chrome-cdp`) plus `browser.cdp_url: http://127.0.0.1:N` gave a verified real navigation (CDP ws resolved in agent.log, example.com visible in chrome's own /json tab list, browser_exec 1.24s/147 chars). Productization = a chromium SIDECAR CONTAINER in the workload (loopback CDP; scratch /dev/shm already mounted; sidecar can run non-root, removing --no-sandbox) β€” proper 1.3.0 through the pipeline, not a boot-script hack. Headline Nous item regardless: the default backend is driverless out of the box.
- **1.2.1: `expose: dashboard` auto-sets `HERMES_DASHBOARD_PUBLIC_URL` from `$(CPLN_GLOBAL_ENDPOINT)`.** Without it, edge TLS termination made the dashboard register CLEARTEXT OAuth callbacks for MCP servers, which auth providers refuse ("cleartext http redirect_uris only for loopback", measured against auth.cpln.io) β€” this was a real user's "MCP not working". With it, UI-only OAuth works: Add server (auth: OAuth) β†’ Authenticate β†’ provider β†’ straight back. Under `expose: api` the CLI paste-back flow is the path. Verified: 55 cpln tools over authenticated OAuth; tokens live on the volume and survive redeploys.
- **Anthropic keys must be WORKSPACE-SCOPED** β€” identity-linked keys 400 on every request (`anthropic-workspace-id is required`) and hermes has NO header hook for the anthropic provider (extra_headers is plumbed for ollama only; adapter hardcodes default_headers). Documented in README; Nous ask filed to widen extra_headers.
- **MCP auth state is unbadged in the UI** β€” Authenticate button and `auth: oauth` chip look identical before/after; the Test button / `hermes mcp test` is the truth (Nous list).
- **The dashboard port-forward is `cpln port-forward {workload} 9119:9119 --gvc {gvc}`** β€” top-level command, positional ports; the `cpln workload port-forward -p` form the README used to show does not exist.
- Model billing follows the seeded config β€” verify with the agent log's `model=` line, never the API response envelope

Expand Down
17 changes: 17 additions & 0 deletions hermes-agent/versions/1.2.1/Chart.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
apiVersion: v2
name: hermes-agent
description: Nous Research Hermes Agent β€” self-hosted, model-agnostic AI agent with persistent memory and browser automation
type: application
version: 1.2.1
appVersion: "2026.8.31"

dependencies:
- name: cpln-common
version: 1.0.0
repository: "oci://ghcr.io/controlplane-com/templates"

annotations:
created: "2026-07-15"
lastModified: "2026-09-02"
category: "ai"
createsGvc: false
194 changes: 194 additions & 0 deletions hermes-agent/versions/1.2.1/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
# Hermes Agent

This app deploys [Hermes Agent](https://github.com/NousResearch/hermes-agent) by Nous Research β€” a self-hosted, model-agnostic AI agent that wraps any LLM with persistent memory, an OpenAI-compatible gateway API, a web dashboard, and browser automation (not functional in this image by default β€” see Browser automation below). You bring the model (an external API key); the agent brings the memory, tools, and interfaces around it.

## Architecture

- **Hermes Agent**: Stateful workload (single replica) running the supervised gateway. OpenAI-compatible API on 8642 (bearer-auth), web dashboard on 9119 (basic-auth); with public access enabled the single canonical HTTPS endpoint fronts one of the two β€” `publicAccess.expose` picks which (API by default).
- **Volumeset**: 10 GiB persistent storage at `/opt/data` β€” the SQLite memory database, sessions, learned skills, and agent config survive restarts and redeploys.
- **Identity + policy**: Least-privilege β€” the workload identity may `reveal` exactly the one prerequisite secret, nothing else.

Single replica is by design: memory is a single-writer SQLite database and upstream forbids two gateways sharing one data directory. On restart, state persists on the volume and the agent resumes; only in-flight work and brief downtime are lost.

## Prerequisites

- **An LLM API key** from your provider β€” Anthropic, OpenAI, or any OpenAI-compatible endpoint (OpenRouter, Ollama, vLLM, …) via `provider: custom`. **Anthropic keys must be WORKSPACE-SCOPED** (created inside a workspace in the Anthropic console) β€” a default/identity-linked key fails every request with `HTTP 400: anthropic-workspace-id is required…`; recreate the key inside a workspace if you see that.
- **A dictionary secret** you create *before* installing (secrets are never passed through values). It holds three values; **name the keys however you like** and map them under `secret.keys` at install β€” an existing secret works unchanged.

| Value | Required | Maps to |
|---|---|---|
| LLM API key for your provider | yes | `secret.keys.apiKey` |
| Bearer token clients present to the gateway API β€” **must be at least 16 characters** | yes | `secret.keys.apiServerKey` |
| Dashboard basic-auth password | when dashboard enabled | `secret.keys.dashboardPassword` |

Hermes **rejects an API server key shorter than 16 characters** (this endpoint dispatches
terminal-capable agent work, so a guessable key is remote code execution) β€” generate one with
`openssl rand -hex 32`. If the key is too short the gateway still starts but the API never
serves, and the workload will not become ready.

Create it in one command (the name `my-hermes-secret` matches the chart's default `secret.name`):

```bash
cpln secret create-dictionary --name my-hermes-secret \
--entry "api-key=YOUR-LLM-API-KEY" \
--entry "api-server-key=$(openssl rand -hex 32)" \
--entry "dashboard-password=YOUR-STRONG-PASSWORD"
```

Pass its name as `secret.name` at install (and override `secret.keys` if your key names differ).

## Configuration

### Image

```yaml
image: nousresearch/hermes-agent:v2026.8.31 # pin the Hermes Agent image tag
```

### Model

```yaml
model:
provider: anthropic # anthropic | openai | custom
name: "" # model override β€” use the provider's exact model ID (e.g. claude-opus-4-6, gpt-5; Anthropic IDs are hyphenated, never dotted). Bare name only β€” the chart adds the anthropic/ prefix itself. Empty = provider default. Recommended for non-anthropic providers.
baseUrl: "" # OpenAI-compatible endpoint; required when provider is "custom"
reasoningEffort: medium # none | low | medium | high β€” use "none" for non-reasoning models
```

**Any other OpenAI-compatible endpoint β€” OpenRouter, Ollama, vLLM, LM Studio, a proxy β€” uses `provider: custom`** with `baseUrl` set and that service's key as your `apiKey`. For example, OpenRouter:

```yaml
model:
provider: custom
baseUrl: https://openrouter.ai/api/v1
name: anthropic/claude-sonnet-4-5
```

**Reasoning effort:** Hermes sends a reasoning effort with every request, and models that do not support reasoning reject it with a `400: Unsupported parameter: 'reasoning.effort'`. Set `reasoningEffort: none` for those (e.g. `gpt-4o`); leave the default for reasoning-capable models (e.g. `gpt-5`, `claude-opus-4-6`).

### Secret

```yaml
secret:
name: my-hermes-secret # name of the dictionary secret you created (see Prerequisites)
keys: # point each field at the key in YOUR secret that holds it
apiKey: api-key
apiServerKey: api-server-key
dashboardPassword: dashboard-password
```

### Dashboard

```yaml
dashboard:
enabled: true # internal-only web UI on port 9119
username: admin # basic-auth username; password is a key in the prerequisite secret
```

### Resources

```yaml
# The min→max spread is the elasticity: idle floor at min, burst toward the
# burst ceiling for heavy agent turns and tool work.
resources:
minCpu: 500m
minMemory: 1Gi
maxCpu: 2000m
maxMemory: 4Gi
```

### Storage

```yaml
volumeset:
capacity: 10 # initial GiB (minimum 10) β€” memory DB, sessions, skills, config
autoscaling:
enabled: false # set true to auto-expand the volume as state grows
maxCapacity: 100 # ceiling in GiB when autoscaling is enabled
minFreePercentage: 10 # scale up when free space drops below this
scalingFactor: 1.2 # multiplier applied on each scale-up
```

### Access

```yaml
publicAccess:
enabled: false # expose the workload on the public canonical HTTPS endpoint
expose: api # api | dashboard β€” which surface the one canonical endpoint fronts (see below)

internalAccess:
type: same-gvc # none | same-gvc | same-org | workload-list
workloads: [] # used with workload-list, e.g. //gvc/GVC/workload/NAME
```

**Choosing the public surface (`publicAccess.expose`):** a workload gets **one** canonical HTTPS endpoint, and it fronts a single port β€” you choose which surface that is. Only meaningful with `publicAccess.enabled: true`.

| `expose` | Canonical endpoint serves | The other surface |
|---|---|---|
| `api` (default) | Gateway API (8642), bearer-auth | Dashboard stays internal β€” reach it via `cpln port-forward` |
| `dashboard` | Web dashboard (9119) behind its basic-auth login | API loses its public endpoint; still reachable internally at `RELEASE-hermes-agent.GVC.cpln.local:8642` |

`expose: dashboard` puts a **basic-auth login form on the internet** β€” the dashboard password in your secret must be strong (`openssl rand -hex 32`), because whoever logs in operates a terminal-capable agent.

## Connecting

| Interface | Where | Auth |
|---|---|---|
| Gateway API (OpenAI-compatible) | From another workload by default (see below). With `publicAccess.enabled: true` and `expose: api` (the default), also on the canonical HTTPS endpoint β€” find it in `status.canonicalEndpoint` (`cpln workload get RELEASE-hermes-agent -o yaml`) | Bearer `API_SERVER_KEY` |
| Web dashboard | Internal by default β€” `cpln port-forward RELEASE-hermes-agent 9119:9119 --gvc GVC`, then `http://localhost:9119`. On the canonical HTTPS endpoint instead with `publicAccess.enabled: true` and `expose: dashboard` | Basic auth (`dashboard.username` + the dashboard password from your secret) |
| From another workload | `RELEASE-hermes-agent.GVC.cpln.local:8642` | Bearer `API_SERVER_KEY` |

Example request against the gateway API:

```bash
curl https://ENDPOINT/v1/chat/completions \
-H "Authorization: Bearer $API_SERVER_KEY" \
-H "Content-Type: application/json" \
-d '{"messages":[{"role":"user","content":"hello"}]}'
```

## Messaging platforms (optional)

Hermes supports chat-platform gateways (Telegram, Discord, Slack, and others). These are **configured after install**, using Hermes's own interactive setup β€” not through this template's values:

```bash
cpln workload exec RELEASE-hermes-agent --gvc GVC --container hermes -- hermes gateway setup
```

Follow the prompts for your platform; the configuration is stored on the data volume. See the [Hermes documentation](https://github.com/NousResearch/hermes-agent) for each platform's requirements, such as bot tokens.

## Browser automation

**Not functional in this image (v2026.8.31) by default.** The agent's browser backend requires binaries the image does not ship (the `browser-use` CLI and a launchable Chrome stack), and installing them by hand still leaves the backend demanding a running Chrome to attach to. The failure is silent: browser requests are answered via `web_extract` with a note that the browser was unavailable β€” read replies carefully rather than assuming the tool ran.

**The one verified path** is attaching a running Chrome over CDP: launch a headless Chromium reachable from the container and run `hermes config set browser.cdp_url http://<host>:<port>` (measured working end to end β€” the agent resolves the CDP websocket and drives real page navigation). A future template version may bundle that Chrome as a sidecar container; until then this is a manual, non-persistent setup. Text-level page fetching (`web_extract`) works normally without any of this.

## Connecting MCP servers that need OAuth

Many MCP servers (including Control Plane's own, `https://mcp.cpln.io/mcp`) authenticate with OAuth. Add the server on the dashboard's MCP page with **Authentication: OAuth**, then:

- **With `publicAccess.expose: dashboard`** (recommended for dashboard use): click **Authenticate** β€” your browser goes to the provider, you sign in, and it redirects straight back to the dashboard. This works because the chart sets the dashboard's public URL automatically; tokens persist on the volume across restarts and redeploys.
- **With `expose: api`** the dashboard has no public URL for OAuth callbacks, so use the one-time CLI flow instead: `cpln workload connect {release}-hermes-agent --gvc {gvc} --container hermes`, then `hermes mcp login <name>` β€” open the printed URL in your browser, and when it lands on a `127.0.0.1:27890/callback` connection error (expected), paste that full URL back into the shell.

If Authenticate fails with `registration failed … cleartext http redirect_uris are only allowed for loopback hosts`, the dashboard does not know its public https URL β€” on this chart that means you are on 1.2.0 or overrode the setting; set `dashboard.public_url` to the canonical endpoint via `hermes config set`.

## Important Notes

- **`publicAccess.enabled: true` publishes a terminal-capable agent to the internet**, guarded only by your bearer token. The agent's terminal backend runs unsandboxed as the container user with full file access, so anyone holding the key can execute work inside the workload. It is off by default β€” before enabling it, use a long random `api-server-key` (`openssl rand -hex 32`) and prefer restricting reach via `internalAccess`.
- **The API server key must be at least 16 characters** β€” Hermes rejects anything shorter, and the workload will not become ready.
- **An OAuth-connected MCP server acts AS THE PERSON WHO AUTHENTICATED IT.** Chat requests can then invoke those tools with that person's permissions β€” for Control Plane's MCP that includes creating and deleting real infrastructure. Connect write-capable MCP servers deliberately, and treat the dashboard password accordingly.
- **The dashboard is internal by default** β€” reach it via `cpln port-forward` (see Connecting). With the default `expose: api`, the public endpoint serves the **API only**, so browsing to it returns 404 at `/` by design β€” `GET /health` returning 200 is how to confirm the workload is up. Putting the dashboard on the internet instead is an explicit choice: `publicAccess.expose: dashboard`, which drops the API's public endpoint and demands a strong dashboard password.
- **A dashboard login that hangs (spinner, request pending forever) is almost always stale BROWSER state, not the server.** Port-forward tunnels that die mid-session leave wedged connections in the browser's profile; later logins then stall while every server surface is healthy. Fix: fully QUIT the browser and reopen (closing the tab is not enough) β€” a private window also works, which is the tell. Confirm the server side in seconds: `curl http://localhost:9119/login` through the tunnel; a fast 200 means the workload is fine. Pages that mount blank in a long-lived session are the same class β€” reload.
- **Single replica by design** β€” memory is single-writer SQLite; do not scale up. State persists on the volume across restarts.
- **The model is external** β€” cost and rate limits are governed by your LLM provider, not this workload.
- **Failed model calls return HTTP 200** with the error inside the body (`"finish_reason": "error"`, `"hermes": {"failed": true}`). A client that checks only the HTTP status will read a provider failure as success β€” inspect the body, or the agent log at `/opt/data/logs/agent.log`.
- **Keep `maxCpu` under 4Γ— `minCpu`** β€” the platform rejects a wider ratio; raise `minCpu` if you raise `maxCpu`.
- **Rotating a value in your prerequisite secret does NOT reach a running workload** β€” `cpln://` references resolve at replica start and are never re-resolved, so the old credential keeps working silently. After any rotation, run `cpln workload force-redeployment RELEASE-hermes-agent --gvc GVC`.
- **Reset** requires `cpln helm uninstall` (deletes the volumeset) β€” changing the secret and redeploying does not wipe existing memory/config on the volume.

## Links

- [Hermes Agent (GitHub)](https://github.com/NousResearch/hermes-agent)
- [Documentation](https://github.com/NousResearch/hermes-agent/blob/main/README.md)
- [Nous Research](https://nousresearch.com/)
- [Control Plane docs](https://docs.controlplane.com/)
Loading
Loading