Skip to content

fix(server): read gateway bearer token at request time and honor HERMES_API_TOKEN#234

Merged
outsourc-e merged 1 commit into
outsourc-e:mainfrom
eudigitalis:fix/runtime-bearer-token
May 3, 2026
Merged

fix(server): read gateway bearer token at request time and honor HERMES_API_TOKEN#234
outsourc-e merged 1 commit into
outsourc-e:mainfrom
eudigitalis:fix/runtime-bearer-token

Conversation

@eudigitalis
Copy link
Copy Markdown
Contributor

Summary

src/server/openai-compat-api.ts has two issues that combine to break the chat surface for any deployment whose Hermes Agent gateway has API_SERVER_KEY set (i.e. anything that isn't an open loopback gateway):

  1. The local BEARER_TOKEN const reads only CLAUDE_API_TOKEN, ignoring the documented HERMES_API_TOKEN env var that the README ("attach the workspace to existing hermes-agent") and the .env.example tell users to set. Looks like a leftover from the Claude → Hermes rename: the analogous const in src/server/gateway-capabilities.ts:222 honors both names, but this local one was never updated.

  2. The const is evaluated at module-load time. Under vite-node SSR (pnpm dev), the module can be loaded in a worker context where process.env doesn't yet contain the values that systemd / EnvironmentFile / .env populated for the parent node process. That freezes the constant to '' permanently, even though the env is correctly populated by the time openaiChat actually runs.

Symptom

Chat UI loads, sessions/skills/memory all work, but every message produces a run with status: error and:

errorMessage: "OpenAI-compatible chat: 401 {\"error\": {..., \"code\": \"invalid_api_key\"}}"

…in <HERMES_HOME>/webui-mvp/runs/<session>/<run>.json. The error format matches what the gateway returns for missing Authorization, not an upstream provider error.

Confirmed via instrumentation in the request handler:

[DEBUG-AUTH] BEARER_TOKEN length: 0
             env.HERMES_API_TOKEN length: 16

Same process.env, two different observed values — the const captured an empty snapshot.

Repro

  1. Run hermes-agent gateway with API_SERVER_KEY=<anything> (so the gateway requires Authorization: Bearer)
  2. Set HERMES_API_TOKEN=<same> in workspace .env
  3. pnpm dev
  4. Open chat, send a message → 401 in webui-mvp/runs/<session>/

Fix

Replace the const with a small getBearerToken() helper that reads env at call time and honors HERMES_API_TOKEN with fallback to CLAUDE_API_TOKEN. Three call sites updated (getDefaultModel, openaiChat Authorization header, and the session-id guard).

No behavior change for setups that already worked (open loopback gateway with no API_SERVER_KEY, or production builds where process.env is fully populated before module load).

Test plan

  • pnpm exec tsc --noEmit — passes for the changed file
  • Manual: with API_SERVER_KEY set on the gateway and HERMES_API_TOKEN set on the workspace, chat works end-to-end via pnpm dev (was 401 before, 200 after)
  • Manual: with no API_SERVER_KEY (loopback / portable), behavior unchanged

…ES_API_TOKEN

src/server/openai-compat-api.ts has two issues that combine to break
the chat surface for any deployment whose Hermes Agent gateway has
`API_SERVER_KEY` set (i.e. anything that isn't an open loopback
gateway):

1. The local `BEARER_TOKEN` const reads only `CLAUDE_API_TOKEN`,
   ignoring the documented `HERMES_API_TOKEN` env var that the README
   tells users to set. Looks like a leftover from the Claude → Hermes
   rename: the const in src/server/gateway-capabilities.ts:222 honors
   both names, but this local one was never updated.

2. The const is evaluated at module-load time. Under vite-node SSR
   (`pnpm dev`), the module can be loaded in a worker context where
   `process.env` doesn't yet contain the values that systemd /
   EnvironmentFile / .env populated for the parent node process.
   That freezes the constant to '' permanently, even though the env
   is correctly populated by the time `openaiChat` actually runs.

Symptom: chat UI loads, sessions/skills/memory all work, but every
message produces a run with `status: error` and:

  errorMessage: "OpenAI-compatible chat: 401 {\"error\": {...,
  \"code\": \"invalid_api_key\"}}"

…in `<HERMES_HOME>/webui-mvp/runs/<session>/<run>.json`. The error
format matches what the gateway returns for missing Authorization, not
an upstream provider error. Confirmed via instrumentation:

  [DEBUG-AUTH] BEARER_TOKEN length: 0
               env.HERMES_API_TOKEN length: 16

Fix: replace the const with a small `getBearerToken()` helper that
reads the env at call time and honors HERMES_API_TOKEN with a fallback
to CLAUDE_API_TOKEN. Three call sites updated (`getDefaultModel`,
`openaiChat` Authorization header, and the session-id guard).

No behavior change for setups that already worked (open loopback
gateway with no API_SERVER_KEY, or production builds where
process.env is fully populated before module load).
@outsourc-e outsourc-e merged commit 2b81b6d into outsourc-e:main May 3, 2026
outsourc-e added a commit that referenced this pull request May 3, 2026
…ack (#265)

* fix(api): replace broken 'authResult as unknown as Response' cast with proper 401

isAuthenticated() returns boolean. The previous pattern:

  const authResult = isAuthenticated(request)
  if (authResult !== true) return authResult as unknown as Response

silenced the TypeScript error but threw HTTPError -> 500 at runtime
because the framework received `false` instead of a Response. This
broke /api/connection-status entirely on protected setups (causing
ONBOARDING_KEY to never persist on fresh installs) and would have
broken the just-merged /api/system-metrics in the same way.

Replace with the canonical pattern used by every other API route:

  if (!isAuthenticated(request)) {
    return json({ error: 'Unauthorized' }, { status: 401 })
  }

Refs #261 (which spotted the pattern in connection-status), #246
(which copied the broken pattern into system-metrics).

* fix(claude-proxy): fall back to /v1/models for /api/available-models on vanilla agent

Vanilla hermes-agent (any version through 2026-05) does not expose
`/api/available-models` \u2014 that endpoint is legacy fork-only. The chat
composer + settings dialog hit `/api/claude-proxy/api/available-models`
expecting it to work, get 404, and fall through to broken UI states
where the model picker is empty.

Fix: when proxying GET /api/available-models and the upstream returns
404, synthesize a compatible `{ models: [...] }` response from
/v1/models filtered by ?provider= so the picker keeps working.

Also: read the bearer token at request time using the same precedence
as the rest of the codebase (HERMES_API_TOKEN || CLAUDE_API_TOKEN ||
module-level BEARER_TOKEN). PR #234 fixed this in openai-compat-api.ts;
this catches the proxy path that was missed.

Refs #261.

---------

Co-authored-by: Aurora release bot <release@outsourc-e.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants