Overview
Hermes Agent is a self-hosted AI agent from Nous Research. You bring the model — an API key for Anthropic, OpenAI, or any OpenAI-compatible endpoint — and the agent brings the memory, tools, and interfaces around it. This template deploys the supervised gateway with an OpenAI-compatible API for your clients, a web dashboard, optional browser automation, and optional external webhooks, with all agent state on a persistent volume.Architecture
- Hermes Agent — A single-replica stateful workload running the supervised gateway. It serves the OpenAI-compatible API on port
8642(bearer auth), the web dashboard on port9119(basic auth), and an optional HMAC-signed webhook listener on port8644. With public access enabled, the single canonical HTTPS endpoint fronts one of these —publicAccess.exposepicks which (the dashboard by default). - Chromium sidecar (optional) — When
browser.enabledis set, a second container running headless Chromium is added to the workload, exposing the Chrome DevTools Protocol (CDP) on loopback. The agent attaches to it so browser tools actually work. Off by default because it is a real CPU/memory cost. - Control Plane MCP (pre-registered, on by default) — With
cplnMcp.enabled, the Control Plane MCP server is registered in the agent’s config so it can manage resources in your cpln org. It stays inert until you authenticate it once via OAuth in the dashboard. - Model — External. Every completion is dispatched to your LLM provider using a key you supply; the workload holds no model weights.
What Gets Created
- Stateful Hermes Agent Workload — Single replica. Gateway API on port
8642(bearer auth), web dashboard on port9119(basic auth), and an optional webhook listener on port8644(HMAC-signed). With public access enabled,publicAccess.exposeselects which one the canonical HTTPS endpoint serves; the others stay internal-only. - Chromium Sidecar Container (optional) — Added to the same workload when
browser.enabled: true. Headless Chromium reachable by the agent over loopback CDP; no published port and no health probe. - Volume Set — 10 GiB persistent storage at
/opt/datafor the SQLite memory database, sessions, learned skills, agent config, and MCP OAuth tokens. Supports optional autoscaling. - Identity & Policy — An identity bound to the workload with
revealaccess to exactly the one prerequisite secret, and nothing else.
This template does not create a GVC. You must deploy it into an existing GVC.
Prerequisites
Hermes Agent needs an LLM API key and a dictionary secret that you create before installing. No credential is passed through values.1
Get an LLM API key
Obtain a key from your provider — Anthropic, OpenAI, or any OpenAI-compatible endpoint (OpenRouter, Ollama, vLLM, LM Studio, a proxy) reached via
provider: custom.Anthropic keys must be workspace-scoped — create the key inside a workspace in the Anthropic console. A default or identity-linked key fails every request with HTTP 400: anthropic-workspace-id is required; recreate it inside a workspace if you see that.2
Generate a gateway API key
Generate the bearer token your clients will present to the gateway API. It must be at least 16 characters — generate one with
openssl rand -hex 32.3
Create a dictionary secret
Create a dictionary secret holding the values below. Name the keys however you like — map them with
secret.keys at install, so an existing secret works unchanged. Pass the secret’s name as secret.name.With the default key names —
my-hermes-secret matches the chart’s default secret.name, so no values override is needed:- A Control Plane
domain— only if you want external webhooks reachable over a custom hostname while the dashboard or API stay public on the canonical endpoint. You create the domain and point it at the workload’s webhook port; see Webhooks for the full steps. This is not needed for the canonical-endpoint webhook path (publicAccess.expose: webhooks).
A missing prerequisite secret wedges the deployment silently —
cpln logs returns nothing because the container never starts. If the workload never becomes ready, run cpln workload get-deployments {release}-hermes-agent --gvc {gvc} -o yaml and read status.versions[].message; it names the missing secret. Recovery is automatic once the secret exists (up to ~6 minutes), or force it with cpln workload force-redeployment.UI
Browse, install, and manage templates visually
CLI
Manage templates from your terminal
Terraform
Declare templates in your Terraform configurations
Pulumi
Declare templates in your Pulumi programs
Configuration
The defaultvalues.yaml for this template:
Model
image— The Hermes Agent container image.model.provider— The LLM provider:
model.name— Model override, e.g.claude-opus-4-6,gpt-5. Empty uses the provider default. Recommended for non-Anthropic providers.model.baseUrl— The OpenAI-compatible endpoint. Required whenprovideriscustom.model.reasoningEffort—none,low,medium, orhigh(defaultmedium).
provider: custom:
model.baseUrl has no effect with provider: anthropic — the Anthropic client ignores it. The template fails at render if you set both, so use provider: custom to route through a proxy.Secret
secret.name— Name of the dictionary secret you created. See Prerequisites.secret.keys.apiKey/secret.keys.apiServerKey/secret.keys.dashboardPassword/secret.keys.webhookSecret— The key names within your secret that hold each value. Override these to match an existing secret rather than renaming its keys.webhookSecretis only read when webhooks are enabled.
Dashboard
dashboard.enabled— Serve the web dashboard on port9119. Public on the canonical HTTPS endpoint under the defaultpublicAccess.expose: dashboard(see Access); otherwise internal-only.dashboard.username— Basic-auth username. The password comes from your secret.dashboard.publicUrl— Base URL used for MCP OAuth callbacks and asset URLs. Empty uses the canonical endpoint (auto-set underexpose: dashboard). Set this only when you front the workload with your own custom domain, so callbacks and asset URLs use that domain instead of the canonical endpoint.
Browser sidecar
browser.enabled— Add a headless-Chromium sidecar so the agent’s browser tools work. Off by default; it is a real CPU/memory cost.browser.image— The pinned community headless-Chromium image (chromedp/headless-shell).browser.cdpPort— The loopback CDP port the app reaches the sidecar on.browser.resources— The sidecar’s own CPU/memory floor and ceiling. KeepmaxCpuunder 4×minCpu(stateful workload).
Control Plane MCP
cplnMcp.enabled— Pre-register the Control Plane MCP server (https://mcp.cpln.io) in the agent’s config. On by default.
cplnMcp.enabled: false to skip registering it. See Connecting MCP servers that need OAuth.
Webhook listener
webhooks.enabled— Turn on the HMAC-signed webhook listener on port8644. Off by default. The signing secret comes fromsecret.keys.webhookSecret.
whatsapp.enabled— Link the agent to a personal WhatsApp account via the image’s built-in Baileys bridge. Off by default.
registry.npmjs.org — covered by the default open outbound) and stores the session on the volume, so it survives restarts. The WhatsApp Cloud (business) API is a separate integration this template does not wire.
Resources
resources.minCpu/resources.minMemory— The idle floor.resources.maxCpu/resources.maxMemory— The burst ceiling for heavy agent turns and tool work.
maxCpu under 4× minCpu — the platform rejects a wider ratio. (The optional browser sidecar has its own separate browser.resources block.)
Upgrading from 1.0.0: the ceiling keys were renamed from
cpu/memory to maxCpu/maxMemory. Values that still carry the old names are refused at render — rename them before upgrading.Storage
volumeset.capacity— Initial volume size in GiB (minimum 10) for the memory database, sessions, skills, config, and MCP OAuth tokens.volumeset.autoscaling.enabled— Automatically expand the volume as state grows. When enabled:maxCapacity— Maximum volume size in GiB.minFreePercentage— Trigger a scale-up when free space drops below this percentage.scalingFactor— Multiply the current capacity by this factor when scaling up.
Access
publicAccess.enabled— Expose the workload on the public canonical HTTPS endpoint. Defaults tofalse. Read the security note below before enabling.publicAccess.expose— Which surface the canonical endpoint fronts:dashboard(default),api, orwebhooks. A workload gets one canonical HTTPS endpoint, and it fronts a single port — this knob picks which. Only meaningful withpublicAccess.enabled: true.
Keeping the API internal is a valid, safer posture for event-driven deployments. The gateway API is a terminal-capable agent behind a single bearer token, so
publicAccess.expose: webhooks — exposing only the HMAC-signed webhook listener while the API stays on the private internal endpoint — is a deliberate hardening choice, not a limitation.
internalAccess.type— Internal firewall scope of the workload:
Connecting
The dashboard’s supported browser access is the public canonical endpoint (default
expose: dashboard). A private install’s dashboard is internal-only.Browser automation
The agent’s browser tools cannot run on the Hermes image alone — it ships no launchable browser, and browser tool calls silently fall back to text-level page fetching (web_extract). Set browser.enabled: true and the chart adds a pinned headless-Chromium container (chromedp/headless-shell) to the workload. Containers in one workload share a network namespace, so the agent reaches Chrome’s DevTools Protocol on loopback (http://127.0.0.1:9222); the chart seeds browser.cdp_url there automatically. No extra setup, and the browser profile is ephemeral (fresh navigation per turn).
- It is a real cost (a second container with its own CPU/memory floor), so it is off by default.
- The sidecar has no published port and no health probe — CDP binds loopback only, which nothing outside the replica can reach.
- Verified on this platform: with the sidecar enabled, the agent resolves the CDP websocket and drives a real page navigation (confirmed in
/opt/data/logs/agent.log) rather than falling back toweb_extract. If the browser is ever unavailable the agent quietly falls back again, so read the reply rather than assuming the tool ran.
Webhooks
Setwebhooks.enabled: true to turn on the listener on port 8644 (the chart also enables the hermes webhook subscribe CLI). Each subscription carries its own HMAC signing secret: hermes webhook subscribe auto-generates one and prints it at creation, or you can pass --secret "$WEBHOOK_SECRET" to reuse the shared secret from secret.keys.webhookSecret (exposed to the container as $WEBHOOK_SECRET). Add the webhookSecret key to your prerequisite secret before enabling webhooks. Sign each event as HMAC-SHA256 of the body in the X-Webhook-Signature header (the gateway recommends the timestamped X-Webhook-Signature-V2 form for replay protection). There are two ways to expose the listener externally:
Both paths are HTTPS. Use
expose: webhooks when webhooks are the only public surface, or a custom domain to keep the dashboard/API public too.
Webhooks on a custom domain
This is the only way to have the dashboard (or API) public on the canonical endpoint AND accept external HTTPS webhooks at the same time. The domain is a prerequisite you own — the chart never creates it (adomain is an org-level resource, so a chart that owned it would delete it on helm uninstall, and only you can add DNS records).
GET / on the webhook domain returns 404 by design — the domain routes to the webhook listener (8644), which only serves /webhooks/<name>, not a root page. A 404 at the root means the domain is wired correctly, not that anything is broken.1
Enable the listener
Install or upgrade with
webhooks.enabled: true and a webhook-secret key in your prerequisite secret. The dashboard stays on the canonical endpoint (default publicAccess.expose: dashboard); the listener runs internally on 8644.2
Create the domain resource
Route
443 → the webhook port 8644 (cpln apply -f domain.yaml):3
Add the two DNS records
Read them back from
cpln domain get webhooks.example.com -o yaml under status.dnsConfig (the CNAME target is the GVC alias):4
Wait for the domain to reach ready
Check
cpln domain get webhooks.example.com — it passes through pendingDnsConfig → pendingCertificate → ready as Let’s Encrypt issues over HTTP-01 (a minute or two once DNS resolves).5
Register a subscription
On the dashboard Webhooks page (or
hermes webhook subscribe <name> --events "*"). Each subscription has its own HMAC secret; copy it, or pass --secret "$WEBHOOK_SECRET" to reuse the shared one.6
Send a signed event
Send to A correct signature returns
https://webhooks.example.com/webhooks/<name> — the body’s HMAC-SHA256 in X-Webhook-Signature:200/202; a wrong one returns 401.Messaging Platforms
Hermes supports chat-platform gateways such as Telegram, Discord, and Slack. These are configured after install using Hermes’s own interactive setup — not through this template’s values:- Telegram works out of the box — a bot token from
@BotFatheris all it needs. - Slack requires an app manifest. Two gotchas: Slack caps an app at 25 slash commands but Hermes’s generated manifest emits ~50 — trim it to 25 or fewer before creating the app, or Slack rejects the manifest. And Slack’s user allowlist fails closed when empty — an empty allowlist silently rejects everyone, so set the allowed users explicitly.
- WhatsApp (personal account) works via the image’s built-in Baileys bridge when you set
whatsapp.enabled: true. Pair it after install: open the dashboard’s WhatsApp setup and scan the QR with WhatsApp on your phone (Settings → Linked devices → Link a device), or runcpln workload exec {release}-hermes-agent --gvc {gvc} --container hermes -- hermes whatsappfor a terminal QR. First pairing installs the bridge’s dependencies to the volume (a few seconds, one-time) and stores the session on the volume, where it is retained across restarts. Once linked, Hermes runs as that WhatsApp account: when someone messages your linked number, Hermes replies with a pairing code that you approve on the dashboard Pairing page (or pre-authorize people with an allowlist during setup). The WhatsApp Cloud (business) API is a separate integration this template does not wire.
Connecting MCP servers that need OAuth
Many MCP servers authenticate with OAuth. Control Plane’s own MCP server (https://mcp.cpln.io/mcp) is pre-registered for you (cplnMcp.enabled: true) — it appears on the dashboard’s MCP page ready to authenticate, so you skip the “add server” step and go straight to Authenticate, a single OAuth click. For any other OAuth MCP server, add it on the MCP page with Authentication: OAuth first. Then:
- With
publicAccess.expose: dashboard(the default): 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 a custom domain, set
dashboard.publicUrlto your domain’s base URL so callbacks and asset URLs use it instead of the canonical endpoint. - With
expose: apithe 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, thenhermes mcp login <name>— open the printed URL, and when it lands on a127.0.0.1:27890/callbackconnection error (expected), paste that full URL back into the shell.
MCP auth state is not badged in the dashboard — the Test button (or
hermes mcp test) is the truth for whether a server is authenticated.Important Notes
publicAccess.enabled: truepublishes a terminal-capable agent to the internet — guarded by a basic-auth login (expose: dashboard) or a bearer token (expose: api). Whoever gets in operates the agent with full file access as the container user, so use a long random password/key (openssl rand -hex 32) and prefer restricting reach viainternalAccess. It is off by default.- The API server key must be at least 16 characters — Hermes rejects anything shorter, and the workload will not become ready.
browser.enabledadds a second container with its own resource floor — a real, ongoing cost. Leave it off unless the agent needs to drive a real browser.- WhatsApp (personal) is opt-in (
whatsapp.enabled: true) and pairs via a QR after install — the bridge’s dependencies install to the volume on first pairing and the session is stored on the volume, retained across restarts. It links your own WhatsApp account and runs as it; new people are gated by a pairing code you approve on the dashboard Pairing page (or an allowlist). The WhatsApp Cloud (business) API is not wired by this template. See Messaging Platforms. - An OAuth-connected MCP server acts as the person who authenticated it — for Control Plane’s own MCP (pre-registered,
cplnMcp.enabled: true) that includes creating and deleting real infrastructure. It stays inert until you authenticate it once via OAuth in the dashboard; connect write-capable MCP servers deliberately. - Single replica by design — memory is a single-writer SQLite database and upstream forbids two gateways sharing one data directory. Do not scale up. State persists on the volume across restarts and the agent resumes; only in-flight work and brief downtime are lost.
- 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
maxCpuunder 4×minCpu(both the app and browser blocks) — the platform rejects a wider ratio on a stateful workload. - Access-knob changes take up to a couple of minutes to propagate — after toggling
publicAccess, re-poll rather than concluding it is broken. - 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, runcpln workload force-redeployment {release}-hermes-agent --gvc {gvc}. - Resetting requires an uninstall — changing the secret and redeploying does not wipe existing memory or config on the volume.
cpln helm uninstalldeletes the volume set.
External References
Hermes Agent Documentation
Official Hermes Agent documentation
Hermes Agent Repository
Source, issues, and release notes
Nous Research
The team behind Hermes Agent
Hermes Agent Template
View the source files, default values, and chart definition