Skip to content

Commit 4091d80

Browse files
committed
feat: added support for frontier model providers
1 parent 3fb94f4 commit 4091d80

4 files changed

Lines changed: 187 additions & 68 deletions

File tree

README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,20 @@ HERMES_PRIMARY_PROVIDER=<provider>
151151
HERMES_MODEL_BASE_URL=http://localhost:11434/v1 # if using a local endpoint
152152
```
153153

154-
For cloud providers, add the relevant API key (e.g. `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`).
154+
For cloud providers, set each role's provider and add the matching API key to `api/.env` — keys are
155+
keyed by provider, not by role:
156+
157+
```
158+
HERMES_PRIMARY_PROVIDER=openai
159+
HERMES_JUDGE_PROVIDER=anthropic
160+
OPENAI_API_KEY=sk-...
161+
ANTHROPIC_API_KEY=sk-ant-...
162+
```
163+
164+
Hermes already knows each cloud provider's base URL, so `HERMES_MODEL_BASE_URL` is only needed for
165+
the `custom` provider (local/self-hosted OpenAI-compatible endpoints). Keys are forwarded by name
166+
into each agent container, so they never appear in the stored command line; the full list of
167+
recognised provider env vars is in `api/.env.example`.
155168

156169
**System mode (no Docker):** set in `api/.env`:
157170

api/.env.example

Lines changed: 89 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -16,66 +16,97 @@ GITHUB_WEBHOOK_SECRET=replace-me
1616
GITHUB_APP_PRIVATE_KEY=
1717
GITHUB_APP_PRIVATE_KEY_PATH=./github.pem
1818

19-
# ── Browser (optional — Camofox) ────────────────────────────────────────────
20-
# Camofox is a self-hosted Firefox-based stealth browser server.
21-
# When set, all hermes browser tools route through Camofox instead of the
22-
# cloud providers. Leave empty to disable (hermes falls back to agent-browser).
23-
# Start Camofox: git clone https://github.com/jo-inc/camofox-browser && cd camofox-browser && make up
24-
# For SANDBOX_MODE=default the agent container automatically uses host.docker.internal;
25-
# for SANDBOX_MODE=none set this to http://localhost:9377.
26-
CAMOFOX_URL=
27-
28-
# ── Hermes Agent CLI ─────────────────────────────────────────────────────────
29-
# Run `npm run hermes:docker` (repo root) to build the agent image and populate
30-
# SANDBOX_MODE / DOCKER_AGENT_IMAGE / HERMES_HOME below automatically.HERMES_BIN=hermes
31-
# HERMES_HOME is the hermes data directory — config, memory notes (MEMORY.md/USER.md),
32-
# skills, and sessions. api/.hermes/ ships a config.base.yaml template and SOUL.md;
33-
# a config.yaml is generated from it at startup using the env-var values below.
34-
# In SANDBOX_MODE=default, MEMORY.md, USER.md, skills/, and config.yaml are bind-mounted
35-
# into each agent container so settings and learning persist across invocations.
19+
# ── Sandbox & Hermes runtime ─────────────────────────────────────────────────
20+
# How and where the Hermes Agent CLI runs. Run `npm run hermes:docker` (repo root) to build
21+
# the agent image and populate SANDBOX_MODE / DOCKER_AGENT_IMAGE / HERMES_HOME automatically.
22+
#
23+
# SANDBOX_MODE:
24+
# none = run hermes as a local subprocess using the system Hermes binary. Set HERMES_HOME
25+
# to your existing Hermes data dir (e.g. ~/.hermes) — Hermes reads its own config.yaml
26+
# from there. HERMES_PRIMARY_MODEL / HERMES_PRIMARY_PROVIDER / HERMES_TIMEOUT_MS still
27+
# apply, passed as CLI flags at invocation time.
28+
# default = run hermes inside a Docker container (recommended; requires DOCKER_AGENT_IMAGE).
29+
SANDBOX_MODE=default
30+
DOCKER_AGENT_IMAGE=hermes-agent:latest
31+
# Name/path of the hermes executable used in SANDBOX_MODE=none.
32+
HERMES_BIN=hermes
33+
# HERMES_HOME is the hermes data directory — config, memory notes (MEMORY.md/USER.md), skills, and
34+
# sessions. api/.hermes/ ships a config.base.yaml template and SOUL.md; a config.yaml is generated
35+
# from it at startup using the env-var values below. In SANDBOX_MODE=default, MEMORY.md, USER.md,
36+
# skills/, and config.yaml are bind-mounted into each agent container so settings and learning
37+
# persist across invocations.
3638
HERMES_HOME=./.hermes
37-
# Model and provider to use for agent runs. Required in SANDBOX_MODE=default — the
38-
# Docker container has no pre-existing Hermes config to fall back on, so these must
39-
# be set or the agent will fail with "No LLM provider configured". In SANDBOX_MODE=none
40-
# they are optional: Hermes reads its own config.yaml (configured via `hermes setup`).
39+
40+
# ── Models & roles ───────────────────────────────────────────────────────────
41+
# Each role is a model + provider pair. The provider name selects a credential from the
42+
# "Provider API keys" section below. Model/provider are passed to Hermes as CLI flags.
43+
# Primary — used for PLAN and IMPLEMENT/REVISE. Required in SANDBOX_MODE=default: the Docker
44+
# container has no pre-existing Hermes config to fall back on, so these must be set or the agent
45+
# fails with "No LLM provider configured". Optional in SANDBOX_MODE=none (Hermes reads config.yaml).
4146
HERMES_PRIMARY_MODEL=
4247
HERMES_PRIMARY_PROVIDER=
43-
# Optional model/provider overrides specifically for review steps. This allows using a more
44-
# capable or independent model to review than the one used for implementation.
48+
# Review — optional override for review steps. Lets a more capable or independent model review
49+
# than the one used for implementation. Falls back to the primary model when unset.
4550
#HERMES_REVIEW_MODEL=
4651
#HERMES_REVIEW_PROVIDER=
47-
# Optional model/provider for Hermes' background auxiliary tasks (context compression, vision
48-
# analysis, web-extract). Point these at a smaller/faster model so secondary work delegated by
49-
# the main agent doesn't compete with it — speeding up the overall run. Both are optional and
50-
# independent; if unset, Hermes uses its own auxiliary defaults ("auto" provider).
52+
# Auxiliary — optional override for Hermes' background tasks (context compression, vision analysis,
53+
# web-extract). Point at a smaller/faster model so secondary work delegated by the main agent
54+
# doesn't compete with it. Both optional and independent; if unset, Hermes uses its own auxiliary
55+
# defaults ("auto" provider).
5156
#HERMES_AUXILIARY_MODEL=
5257
#HERMES_AUXILIARY_PROVIDER=
53-
# Optional model/provider for the completion judge that decides, after each IMPLEMENT/REVISE
54-
# pass, whether the plan's acceptance criteria are actually met (and continues the agent with a
55-
# critique if not). Falls back to the review model, then the primary model. A weak model makes
56-
# an unreliable judge — prefer a capable one.
58+
# Judge — optional override for the completion judge that decides, after each IMPLEMENT/REVISE pass,
59+
# whether the plan's acceptance criteria are met (and continues the agent with a critique if not).
60+
# Falls back to the review model, then the primary model. A weak model makes an unreliable judge —
61+
# prefer a capable one.
5762
#HERMES_JUDGE_MODEL=
5863
#HERMES_JUDGE_PROVIDER=
59-
# Hard wall-clock ceiling (ms) for a single Hermes invocation — the process is killed
60-
# regardless of activity once this elapses. For local inference with large models, increase
61-
# this; a full implementation cycle can take well over an hour on modest hardware.
64+
65+
# ── Provider API keys (cloud / remote inference) ─────────────────────────────
66+
# Credentials are keyed by PROVIDER, not by role: each HERMES_*_PROVIDER above names a provider
67+
# (e.g. openai, anthropic) and Hermes reads that provider's key from the matching standard env var
68+
# below — and already knows its base URL, so no per-provider URL is needed. (The `custom` provider
69+
# is the exception: its endpoint is HERMES_MODEL_BASE_URL; local Ollama needs no key at all.) Set
70+
# only the keys for providers you actually use. In SANDBOX_MODE=default these are forwarded by name
71+
# into each agent container (the value never appears in the stored command line); in
72+
# SANDBOX_MODE=none the Hermes subprocess inherits them directly. Recognised: OPENAI_API_KEY,
73+
# ANTHROPIC_API_KEY, GEMINI_API_KEY, GOOGLE_API_KEY, GROQ_API_KEY, MISTRAL_API_KEY, DEEPSEEK_API_KEY,
74+
# OPENROUTER_API_KEY, XAI_API_KEY, TOGETHER_API_KEY, FIREWORKS_API_KEY, PERPLEXITY_API_KEY, NOUS_API_KEY.
75+
#OPENAI_API_KEY=
76+
#ANTHROPIC_API_KEY=
77+
78+
# ── Inference tuning ─────────────────────────────────────────────────────────
79+
# Hard wall-clock ceiling (ms) for a single Hermes invocation — the process is killed regardless
80+
# of activity once this elapses. For local inference with large models, increase this; a full
81+
# implementation cycle can take well over an hour on modest hardware.
6282
HERMES_TIMEOUT_MS=7200000
63-
# Maximum context window size (tokens) passed to Hermes as context_length in config.yaml.
64-
# Must match the model's actual context window — too large causes OOM on the inference
65-
# server; too small causes premature compression. Omit to let Hermes use its model default.
83+
# Maximum context window size (tokens) passed to Hermes as context_length in config.yaml. Must
84+
# match the model's actual context window — too large causes OOM on the inference server; too small
85+
# causes premature compression. Omit to let Hermes use its model default.
6686
HERMES_CONTEXT_LENGTH=131072
67-
# Fraction of the context window (0–1) at which Hermes compresses older messages to free
68-
# space. Lower values compress more aggressively; higher values preserve more history at
69-
# the cost of context pressure. Omit to use the Hermes default (0.5).
87+
# Fraction of the context window (0–1) at which Hermes compresses older messages to free space.
88+
# Lower values compress more aggressively; higher values preserve more history at the cost of
89+
# context pressure. Omit to use the Hermes default (0.5).
7090
HERMES_COMPRESS_THRESHOLD=0.80
71-
# OpenAI-compatible base URL for the model provider. In SANDBOX_MODE=default this is
72-
# automatically rewritten to host.docker.internal so the agent container can reach a
73-
# local inference server running on the host. Omit to use the Hermes default (Ollama at
74-
# http://localhost:11434/v1).
91+
# OpenAI-compatible base URL for the model provider. Only needed for the `custom` provider
92+
# (local/self-hosted endpoints) — named cloud providers already know their own URL. In
93+
# SANDBOX_MODE=default this is automatically rewritten to host.docker.internal so the agent
94+
# container can reach a local inference server running on the host. Omit to use the Hermes default
95+
# (Ollama at http://localhost:11434/v1).
7596
HERMES_MODEL_BASE_URL=http://localhost:11434/v1
76-
# Diagnostic: when true, logs a compact identity line (type/name/model/usage/attribute keys)
77-
# for every incoming trace span. Use to inspect exactly how Hermes traces compression and
78-
# other auxiliary calls. Noisy — leave off in normal operation.
97+
98+
# ── Browser (optional — Camofox) ────────────────────────────────────────────
99+
# Camofox is a self-hosted Firefox-based stealth browser server. When set, all hermes browser tools
100+
# route through Camofox instead of the cloud providers. Leave empty to disable (hermes falls back to
101+
# agent-browser). Start Camofox: git clone https://github.com/jo-inc/camofox-browser && cd camofox-browser && make up
102+
# For SANDBOX_MODE=default the agent container automatically uses host.docker.internal; for
103+
# SANDBOX_MODE=none set this to http://localhost:9377.
104+
CAMOFOX_URL=
105+
106+
# ── Observability ────────────────────────────────────────────────────────────
107+
# Diagnostic: when true, logs a compact identity line (type/name/model/usage/attribute keys) for
108+
# every incoming trace span. Use to inspect exactly how Hermes traces compression and other
109+
# auxiliary calls. Noisy — leave off in normal operation.
79110
#LANGFUSE_DEBUG_SPANS=false
80111

81112
# ── Orchestration policy ─────────────────────────────────────────────────────
@@ -106,29 +137,20 @@ QUEUE_BACKOFF_BASE_MS=15000
106137
QUEUE_LOCK_TTL_MS=3600000
107138

108139
# ── Workspace / git ──────────────────────────────────────────────────────────
109-
# Path to the directory where per-job git worktrees are cloned. When running via
110-
# docker-compose, this is overridden to ${PWD}/workspaces and mounted at that same
111-
# absolute path inside the container so sibling agent containers (spawned via the
112-
# Docker socket) can reference workspace directories by the same path the host sees.
140+
# Path to the directory where per-job git worktrees are cloned. When running via docker-compose,
141+
# this is overridden to ${PWD}/workspaces and mounted at that same absolute path inside the
142+
# container so sibling agent containers (spawned via the Docker socket) can reference workspace
143+
# directories by the same path the host sees.
113144
WORKSPACE_ROOT=./.workspaces
114145
GIT_AUTHOR_NAME=Hermes Agent
115146
GIT_AUTHOR_EMAIL=hermes@users.noreply.github.com
116147
# Branch name template; {issue} is replaced with the issue number.
117148
BRANCH_PREFIX=hermes/issue-
118-
# Optional dedicated SSH deploy key for cloning/pushing DASHBOARD-origin jobs (created from
119-
# the UI) against an SSH remote. Leave UNSET to use the host's own SSH setup (ssh-agent,
120-
# ~/.ssh/config, default keys) exactly as a normal `git clone git@…` would — this is the
121-
# usual case when the orchestrator runs on a host that already has repo access. Set it only
122-
# to force an isolated key (it then ignores the host agent via IdentitiesOnly). Powerful — it
123-
# can push to any repo it's authorised for; mount it read-only and never expose it to the
124-
# agent container (git runs on the orchestrator host, not the agent sandbox).
149+
# Optional dedicated SSH deploy key for cloning/pushing DASHBOARD-origin jobs (created from the UI)
150+
# against an SSH remote. Leave UNSET to use the host's own SSH setup (ssh-agent, ~/.ssh/config,
151+
# default keys) exactly as a normal `git clone git@…` would — this is the usual case when the
152+
# orchestrator runs on a host that already has repo access. Set it only to force an isolated key
153+
# (it then ignores the host agent via IdentitiesOnly). Powerful — it can push to any repo it's
154+
# authorised for; mount it read-only and never expose it to the agent container (git runs on the
155+
# orchestrator host, not the agent sandbox).
125156
#GIT_SSH_KEY_PATH=/home/youruser/.ssh/olympian_deploy_key
126-
127-
# ── Sandbox ──────────────────────────────────────────────────────────────────
128-
# none = run hermes as a local subprocess using the system Hermes binary.
129-
# Set HERMES_HOME to your existing Hermes data directory (e.g. ~/.hermes) — Hermes
130-
# reads its own config.yaml from there. HERMES_PRIMARY_MODEL / HERMES_PRIMARY_PROVIDER
131-
# / HERMES_TIMEOUT_MS still apply as they are passed as CLI flags at invocation time.
132-
# default = run hermes inside a Docker container (recommended; requires DOCKER_AGENT_IMAGE).
133-
SANDBOX_MODE=default
134-
DOCKER_AGENT_IMAGE=hermes-agent:latest

api/src/agent/agent.utility.spec.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,51 @@ describe('buildAgentSpec', () => {
158158
expect.arrayContaining(['--model', 'anthropic/claude-sonnet-4.6', '--provider', 'anthropic']),
159159
);
160160
});
161+
162+
it('forwards set provider API keys into the container by name only (no secret in args)', () => {
163+
const prev = process.env.OPENAI_API_KEY;
164+
process.env.OPENAI_API_KEY = 'sk-secret-value';
165+
try {
166+
const spec = buildAgentSpec({
167+
sandboxMode: 'default',
168+
hermesBin: 'hermes',
169+
dockerImage: 'img',
170+
cwd: '/w',
171+
prompt: 'test prompt',
172+
});
173+
// name is forwarded...
174+
const idx = spec.args.indexOf('OPENAI_API_KEY');
175+
expect(idx).toBeGreaterThan(-1);
176+
expect(spec.args[idx - 1]).toBe('--env');
177+
// ...but the value never appears in the args (which get persisted to AgentRun.command).
178+
expect(spec.args.join(' ')).not.toContain('sk-secret-value');
179+
} finally {
180+
if (prev === undefined) {
181+
delete process.env.OPENAI_API_KEY;
182+
} else {
183+
process.env.OPENAI_API_KEY = prev;
184+
}
185+
}
186+
});
187+
188+
it('does not forward provider API keys that are unset', () => {
189+
const prev = process.env.GROQ_API_KEY;
190+
delete process.env.GROQ_API_KEY;
191+
try {
192+
const spec = buildAgentSpec({
193+
sandboxMode: 'default',
194+
hermesBin: 'hermes',
195+
dockerImage: 'img',
196+
cwd: '/w',
197+
prompt: 'test prompt',
198+
});
199+
expect(spec.args).not.toContain('GROQ_API_KEY');
200+
} finally {
201+
if (prev !== undefined) {
202+
process.env.GROQ_API_KEY = prev;
203+
}
204+
}
205+
});
161206
});
162207

163208
describe('generateHermesConfig auxiliary model', () => {

api/src/agent/agent.utility.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,36 @@ import { type LangfuseEvent } from '../langfuse/langfuse.model.js';
1111
const HERMES_CONTAINER_HOME = '/root/.hermes';
1212
const CONTAINER_WORKDIR = '/workspace';
1313

14+
/**
15+
* Provider credential env vars forwarded into the agent container (SANDBOX_MODE=default).
16+
*
17+
* A model role selects a provider via `--provider <name>` (e.g. HERMES_JUDGE_PROVIDER=anthropic);
18+
* Hermes then reads that provider's API key from the standard env var below and already knows the
19+
* provider's base URL, so no per-provider base_url config is needed (the `custom` provider is the
20+
* exception — its endpoint comes from HERMES_MODEL_BASE_URL). Forwarding every present key into
21+
* each container also covers Hermes' background auxiliary tasks (compression/vision/web-extract),
22+
* which run in-process under the primary and so need their provider's key in the same container.
23+
*
24+
* Forwarded by NAME ONLY (`--env OPENAI_API_KEY`) so the value is pulled from the orchestrator's
25+
* own environment by the docker CLI and never lands in the args array — which is persisted verbatim
26+
* to AgentRun.command. Add new providers here as needed.
27+
*/
28+
export const PROVIDER_CREDENTIAL_ENV = [
29+
'OPENAI_API_KEY',
30+
'ANTHROPIC_API_KEY',
31+
'GEMINI_API_KEY',
32+
'GOOGLE_API_KEY',
33+
'GROQ_API_KEY',
34+
'MISTRAL_API_KEY',
35+
'DEEPSEEK_API_KEY',
36+
'OPENROUTER_API_KEY',
37+
'XAI_API_KEY',
38+
'TOGETHER_API_KEY',
39+
'FIREWORKS_API_KEY',
40+
'PERPLEXITY_API_KEY',
41+
'NOUS_API_KEY',
42+
] as const;
43+
1444
export interface AgentSpecParams {
1545
sandboxMode: 'none' | 'default';
1646
hermesBin: string;
@@ -124,6 +154,15 @@ export function buildAgentSpec(p: AgentSpecParams): SpawnSpec {
124154
`HERMES_LANGFUSE_BASE_URL=http://host.docker.internal:${langfusePort}/langfuse`,
125155
);
126156

157+
// Forward provider API keys (by name only) so the role's selected provider can
158+
// authenticate. Name-only keeps the secret out of args (persisted to AgentRun.command);
159+
// docker resolves the value from this process's env at spawn time.
160+
for (const name of PROVIDER_CREDENTIAL_ENV) {
161+
if (process.env[name]) {
162+
args.push('--env', name);
163+
}
164+
}
165+
127166
const containerName = `olympian-agent-${randomUUID()}`;
128167

129168
args.push('--name', containerName);

0 commit comments

Comments
 (0)