You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A Kubernetes operator (Go) for deploying and managing
Nous Research Hermes Agent
gateways declaratively via a HermesAgent Custom Resource.
Full design: docs/specification.md. Pinned upstream:
Hermes Agent v2026.6.5 (vendored at third_party/hermes-agent).
What it does
One HermesAgent CR ⇒ one gateway = one Deployment(Recreate, replicas≤1) + one
shared RWO PVC (sub-paths for data, ~/.local, Homebrew, and the dind store) +
ConfigMaps + Service (+ optional Ingress) + an in-pod reloader sidecar. The
gateway is a singleton (SQLite + gateway.lock), so the operator enforces
replicas ∈ {0,1} and Recreate so the old pod releases the volume lock before
the new one starts.
Highlights:
Renders config.yaml from typed CRD fields + deep-merged extraConfig.
Credentials via Secret refs — the operator never reads secret material
(a Secret's resourceVersion feeds the config hash to trigger rollouts).
Single shared PVC with a settable size.
Model providers — typed model.providers[] for hermes built-ins
(anthropic/openai/xai/openrouter/…, selected by name/alias) and custom
OpenAI-compatible endpoints (with per-model context windows + key injection).
MCP servers — typed mcp.servers[] (stdio + http/sse) with per-server tool
filtering and Secret-backed credentials injected for ${VAR} interpolation.
Bitwarden secrets — secrets.bitwarden syncs secrets via the bws machine
account, with a Secret-backed access token and a custom/self-hosted serverURL.
Named hermes profiles — profile.name runs the agent under an isolated
profiles/<name>/ home (own config/SOUL/skills/state) via HERMES_HOME.
Pre-install of optional deps onto the PVC (pip SDKs for channels/backends,
honcho-ai, Apptainer for singularity) so they survive restarts.
Declarative skill activation and package installation — pip (init
container, persisted on the PVC via PYTHONPATH) + Homebrew (no sudo).
Operator-managed Docker-in-Docker sidecar for the docker terminal backend,
and an in-cluster kubeconfig written to ~/.kube/config.
A single shared Ingress config defaulted across every HTTP surface, with
auto webhook ingress for webhook-capable channels.
Full podTemplate overlay strategic-merged over the operator's base pod,
with operator invariants (shared-PVC mounts, config-hash, root-start) re-asserted.
images/reloader/Dockerfile (FROM agent + Go binary)
HermesAgent reference
apiVersion: hermes.nousresearch.io/v1alpha1, kind: HermesAgent. Everything below
is under .spec. Optional fields show their default; — means no default (unset).
Config-rendered fields note their config.yaml target as → key.
Top level
Field
Type
Default
Notes
image
string
required
Agent container image (the operator's derived image).
imagePullPolicy
string
IfNotPresent
imagePullSecrets
[]LocalObjectReference
—
Pull secrets (name refs).
replicas
int (0–1)
1
Singleton; 0 pauses (scales the Deployment to 0).
presetRef.name
string
—
A HermesConfigPreset deep-merged under this spec (CR wins).
profile.name
string
—
Run under a named hermes profile: config/SOUL/skills/state live in /opt/data/profiles/<name>/ (HERMES_HOME points there). Pattern ^[a-z0-9][a-z0-9_-]{0,63}$; reserved names rejected.
soul
string
—
Persona rendered to $HERMES_HOME/SOUL.md (the profile dir when profile is set).
env
[]corev1.EnvVar
—
Extra agent-container env (appended after operator vars).
envFrom
[]corev1.EnvFromSource
—
Mirrored onto the agent container.
authJSONBootstrapSecretRef
SecretKeyRef
—
Seeds auth.json once on first boot.
extraConfig
object (free-form)
—
Deep-merged into config.yaml (preserved verbatim).
Governs the operator's init containers. The main agent container always starts as root: the image's s6-overlay /init requires root for cont-init bootstrap, then drops the hermes process to hermesUID via s6-setuidgid.
Selecting model.provider = a built-in alias resolves the key env automatically
(claude→ANTHROPIC_API_KEY, grok→XAI_API_KEY, openai→OPENROUTER_API_KEY, …);
for a custom active provider the operator derives model.base_url/api_mode from it.
→ HONCHO_BASE_URL. Honcho is "in use" when this or apiKeySecretRef is set.
honcho.apiKeySecretRef
SecretKeyRef
—
→ HONCHO_API_KEY (hosted Honcho).
honcho.installPackage
*bool
true
Pre-install honcho-ai onto the PVC (via the pip-install init).
mcp (→ mcp_servers:)
mcp.servers[] declares Model Context Protocol servers, rendered to config.yaml
mcp_servers keyed by name. A server is stdio (set command) or http/sse
(set url) — exactly one. Credentials go in secretEnv (a Secret key injected as a
container env var) and are referenced with ${ENVNAME} inside headers/env values
(hermes interpolates them at connect time; values pass through the operator untouched).
mcp.servers[] — MCPServerSpec
Field
Type
Default
Notes
name
string
required
Keys the server in mcp_servers (unique).
enabled
*bool
true
Toggle the server.
transport
stdio|http|sse
—
stdio implied by command; url implies http unless set to sse.
command
string
—
stdio: subprocess to launch. Exactly one of command/url.
args
[]string
—
stdio: command args.
env
map[string]string
—
stdio: subprocess env; values may use ${VAR}.
url
string
—
http/sse: endpoint. Exactly one of command/url.
headers
map[string]string
—
http/sse: request headers; values may use ${VAR} (e.g. Bearer ${TOK}).
sslVerify
*bool
true
http/sse: TLS verification.
timeoutSeconds
*int32
120
Per tool-call timeout.
connectTimeoutSeconds
*int32
60
Initial connection timeout.
supportsParallelToolCalls
*bool
false
stdio: allow concurrent tool calls.
tools.include / tools.exclude
[]string
—
Filter exposed tools (set at most one).
secretEnv[]
[]{name,secretRef}
—
Inject a Secret key as env var name; reference via ${name}.
extraConfig
object
—
Deep-merged into this server (e.g. oauth, sampling); typed fields win.
secrets (→ secrets:)
secrets.bitwarden wires the agent to Bitwarden Secrets Manager
(machine account + the bws CLI), rendered to config.yaml secrets.bitwarden. The
machine-account access token is sensitive, so it is not rendered into config —
accessTokenSecretRef is injected as the env var named by accessTokenEnv (default
BWS_ACCESS_TOKEN) and hermes reads it at runtime. serverURL points bws at a
custom region or self-hosted instance (e.g. https://vault.bitwarden.eu, or a
Vaultwarden URL); leave it empty for the US cloud.
secrets.bitwarden — BitwardenSpec
Field
Type
Default
Notes
enabled
*bool
false
Master switch for Bitwarden sync. When true, accessTokenSecretRef is required.
accessTokenSecretRef
SecretKeyRef
—
Machine-account token; injected as env var accessTokenEnv.
accessTokenEnv
string
BWS_ACCESS_TOKEN
Name of the env var holding the access token.
projectID
string
—
Bitwarden project UUID to sync from.
serverURL
string
—
Custom/self-hosted endpoint (→ server_url). Empty = US cloud.
cacheTTLSeconds
*int32
300
In-process cache TTL for fetched secrets.
overrideExisting
*bool
true
Let Bitwarden values replace existing env vars (never the token var itself).
autoInstall
*bool
true
Let the agent download the bws binary on demand (needs egress).
ingress (shared, → Ingress objects)
Top-level spec.ingress is the shared Ingress config. When enabled (and
dashboard.enabled), the dashboard is exposed at ingress.path. Its
host/className/annotations/tls/pathType are inherited by apiServer and
channel-webhook ingresses unless they set their own.
Field
Type
Default
Notes
ingress.enabled
bool
false
ingress.host
string
—
Base host; required (here or per-surface) when an ingress is enabled.
ingress.className
string
—
→ spec.ingressClassName.
ingress.path
string
/
ingress.pathType
Exact|Prefix|ImplementationSpecific
Prefix
ingress.annotations
map[string]string
—
Merged verbatim (nginx/cert-manager/etc.).
ingress.tls[]
[]{hosts[],secretName}
—
TLS blocks.
apiServer (→ env, port 8642) · dashboard (→ env, port 9119)
spec:
runAsRoot: falseserviceAccount: { create: false, name: agent-sa, automountToken: true }storage: { size: 50Gi, storageClassName: proxmox-local-ext4, reclaimPolicy: Retain }runtime:
terminalBackend: docker # operator injects a DinD sidecar on the shared PVCkubeconfig: { enabled: true } # ~/.kube/config from the pod SA (kubectl works in-pod and in dind)packages:
brew: [kubectl] # persisted on the PVC, also bind-mounted into dind
OpenAI-compatible API server + extraConfig escape hatch
spec:
apiServer:
enabled: truehost: 0.0.0.0corsOrigins: ["*"]keySecretRef: { name: agent-secret, key: API_SERVER_KEY }ingress: { enabled: true } # inherits spec.ingress host/tlsingress: { enabled: true, host: api.example.com }# Anything not modeled as a typed field can be merged into config.yaml:extraConfig:
fallback_model: { provider: openrouter, model: anthropic/claude-sonnet-4 }auxiliary:
compression: { provider: openrouter, model: google/gemini-3-flash }
MCP servers (stdio + http with a secret token)
spec:
mcp:
servers:
# stdio: launched as a subprocess; token interpolated into its env.
- name: githubcommand: npxargs: ["-y", "@modelcontextprotocol/server-github"]env:
GITHUB_PERSONAL_ACCESS_TOKEN: ${GH_TOKEN}secretEnv:
- name: GH_TOKENsecretRef: { name: mcp-secrets, key: github-token }tools:
include: [create_issue, get_issue] # narrow the exposed tools# http/sse: remote endpoint with a bearer token from a Secret.
- name: remotetransport: sseurl: https://mcp.example.com/sseheaders:
Authorization: Bearer ${REMOTE_TOKEN}secretEnv:
- name: REMOTE_TOKENsecretRef: { name: mcp-secrets, key: remote-token }extraConfig: # long-tail knobs (oauth, sampling)sampling: { enabled: true, max_rpm: 10 }
Bitwarden secrets (self-hosted server)
spec:
secrets:
bitwarden:
enabled: trueserverURL: https://vault.example.com # custom region / self-hosted / VaultwardenprojectID: 11111111-2222-3333-4444-555555555555# The token rides in as env BWS_ACCESS_TOKEN (never written to config.yaml).accessTokenSecretRef: { name: bw-creds, key: token }
Run under a named hermes profile
spec:
profile:
name: staging # HERMES_HOME=/opt/data/profiles/staging; config/SOUL/# skills/memory/sessions isolated there. Gateway runs# under `staging`; the default home is untouched.
A complete, production CR (custom endpoint + docker + kubeconfig + channels +
searxng + honcho + dashboard ingress) lives at
k8s-cluster/ouchi/bne1-cluster1/hermes/lana-hermesagent.yaml.
make generate manifests # codegen + CRD/RBAC manifests
make build # build operator + reloader
make test# unit + envtest integration
make helm-lint helm-template # render/validate the chart
kustomize build config/default
# Images (single-arch local, or buildx multi-arch + push to Harbor)
make docker-build
make docker-buildx IMG_REGISTRY=harbor.bne1.ouchi.com.au/applications VERSION=v0.1.0
make helm-push