Skip to content

[Bug]: v0.8.7 never sends x-api-key to a self-hosted Code Interpreter — getCodeApiAuthHeaders returns {} or a Bearer JWT, breaking every interpreter written to the documented contract #14759

Description

@knormoyle

What happened?

Companion issue (interpreter side): "Feature request: verify LibreChat-minted JWTs (LibreChat v0.8.6+ no longer sends x-api-key)" — filed at usnavy13/LibreCodeInterpreter · usnavy13/LibreCodeInterpreter#121

The two issues are halves of one incompatibility: LibreChat stopped sending x-api-key, and interpreters implementing the documented contract have no way to accept what it sends instead. Either side closing the gap would resolve it.

What happened?

With a self-hosted code interpreter configured per the Code Interpreter docs — LIBRECHAT_CODE_BASEURL + LIBRECHAT_CODE_API_KEY — every execution fails, and reading the shipped code shows why: LIBRECHAT_CODE_API_KEY is never read when building the outbound request headers.

The docs describe the env var as the global setup path:

Global setup: use LIBRECHAT_CODE_API_KEY environment variable in the .env file of your project (provides access to all users)
…
Include the API key in your requests using the x-api-key header

But in v0.8.7, x-api-key is never attached.

Root cause (from the shipped bundle)

/app/packages/api/dist/index.cjs:

function isManagedCodeApiJwtMode() {
	const provider = process.env.CODEAPI_AUTH_PROVIDER;
	return provider === "librechat-jwt" || provider === "both";
}

function isCodeApiJwtAuthEnabled() {
	return isManagedCodeApiJwtMode() || isEnabled(process.env.CODEAPI_JWT_ENABLED);
}

async function getCodeApiAuthHeaders(req) {
	if (!req || !isCodeApiJwtAuthEnabled()) return {};   // ← no API-key fallback
	const token = await mintCodeApiToken(req);
	return token ? { Authorization: `Bearer ${token}` } : {};
}

process.env.LIBRECHAT_CODE_API_KEY appears nowhere in this function or its callees. There are exactly two possible return values: {} when JWT auth is off, or { Authorization: "Bearer <jwt>" } when it is on. Neither carries x-api-key.

That empty object is spread into every code-API request. @librechat/agents/dist/cjs/tools/CodeExecutor.cjs:

const fetchOptions = {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "User-Agent": "LibreChat/1.0",
    ...await resolveCodeApiAuthHeaders(authHeaders)   // ← {} or { Authorization: Bearer … }
  },
  body: JSON.stringify(postData)
};

So the request goes out with either only Content-Type and User-Agent, or those plus a Bearer JWT. An interpreter expecting x-api-key returns 401 in both cases, which is then mapped to a generic message:

if (response.status === 401 || response.status === 403)
  return CODE_API_AUTHORIZATION_ERROR_MESSAGE;
// 'Code execution is not authorized. Verify access before trying again.'

The same getCodeApiAuthHeaders is used for the file paths in api/server/services/Files/Code/crud.js (download, delete, upload), so file operations are affected identically.

Proof: a real minted token is rejected

The token below was minted by LibreChat itself, extracted by calling the exported function directly, then replayed against the interpreter:

TOKEN=$(docker compose exec -T api node -e "
const api=require('/app/packages/api/dist/index.cjs');
api.getCodeApiAuthHeaders({user:{id:'<userId>',role:'ADMIN'},authStrategy:'local'})
  .then(h=>process.stdout.write(h.Authorization.replace('Bearer ','')));
")

curl -i -s -X POST http://<interpreter>:8000/exec \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"lang":"py","code":"print(1)"}'
HTTP/1.1 401 Unauthorized
{"error":"API key is required"}

The same interpreter, same endpoint, with -H "x-api-key: $LIBRECHAT_CODE_API_KEY" instead executes normally — {"lang":"py","code":"print(2+2)"} returns {"stdout":"4\n","stderr":"","files":[]}.

Minting works correctly. Calling getCodeApiAuthHeaders directly returns a valid EdDSA JWT:

{
  "iss": "librechat", "aud": "codeapi", "sub": "<userId>",
  "tenant_id": "default", "role": "ADMIN",
  "principal_source": "librechat_jwt",
  "auth_context_hash": "414e0ae8…"
}

(header: {"alg":"EdDSA","typ":"JWT","kid":"lc-codeapi-2026-05"})

So the failure is not misconfiguration on either side. LibreChat sends a Bearer JWT; the interpreter — written to the documented x-api-key contract — rejects it. There is no configuration that makes v0.8.7 send x-api-key.

Steps to reproduce

  1. Deploy any codeapi-compatible interpreter that authenticates with x-api-key.
  2. Set LIBRECHAT_CODE_BASEURL and LIBRECHAT_CODE_API_KEY in .env.
  3. Create an agent with Run Code enabled and send print("hello").
  4. LibreChat logs Code execution is not authorized. Verify access before trying again.
  5. The interpreter 401s. A rejected request is not logged as a failed execution, so from the interpreter side the service appears idle — which is what makes this hard to diagnose.

Both auth configurations fail, for different reasons. I tried each:

Configuration What LibreChat sends Result
No CODEAPI_* variables set (no auth header at all) 401
CODEAPI_AUTH_PROVIDER=librechat-jwt + valid ed25519 key Authorization: Bearer <JWT> 401

The second case is worth stressing: minting succeeds and a well-formed token is sent, but an interpreter built to the documented contract has nothing to verify it against. There is no configuration under which v0.8.7 sends x-api-key.

Who this breaks

This is not a hypothetical. The codeapi contract has independent implementations built against the published docs, and the ones using x-api-key are broken by this change:

Implementation Auth Status against v0.8.7
usnavy13/LibreCodeInterpreter x-api-key only Broken — 401 on every call. 112★, prebuilt GHCR images, the most-adopted community implementation
martvaha/code-interpreter documents CODEAPI_JWT_* verification Works — but its README calls itself a proof of concept, and it requires a Docker socket
ronith256/Code-Interpreter-LibreChat not verified Unknown

The one implementation that survives the change is the one that reverse-engineered the JWT scheme. Anyone following the current documentation builds a server LibreChat cannot authenticate to.

I have opened a companion request on LibreCodeInterpreter asking it to verify LibreChat-minted JWTs (see link at top). But that only fixes one interpreter — the docs would still describe a contract LibreChat does not implement, and every future codeapi server written from them would hit the same wall.

Expected behavior

Either of:

  • getCodeApiAuthHeaders falls back to { 'x-api-key': process.env.LIBRECHAT_CODE_API_KEY } when JWT auth is not enabled — matching the documented behaviour; or
  • the docs are updated to state that self-hosted deployments now require the CODEAPI_JWT_* configuration, with the required variables listed and an example of what the interpreter must do to verify.

The first seems preferable: it keeps the documented path working, and the JWT mode remains available for deployments that want per-user claims. A two-line fallback would do it:

async function getCodeApiAuthHeaders(req) {
	if (!req || !isCodeApiJwtAuthEnabled()) {
		const key = process.env.LIBRECHAT_CODE_API_KEY;
		return key ? { 'x-api-key': key } : {};
	}
	const token = await mintCodeApiToken(req);
	return token ? { Authorization: `Bearer ${token}` } : {};
}

If the JWT scheme is intended to fully replace API keys, then documenting it is the equivalent fix — the CODEAPI_JWT_* variables appear in neither the Code Interpreter docs nor .env.example, so third-party interpreters currently have no way to implement verification except by reading the compiled bundle, which is how I arrived at the claim structure above.

Secondary: the error message sends you to the wrong subsystem

Code execution is not authorized. Verify access before trying again. reads as a permissions failure. I spent a long time in the RBAC system before looking at headers — verifying RUN_CODE: { USE: true } on both roles, confirming interface: runCode: true parsed at startup, checking the agent capability, and inspecting the roles collection directly. All were correct; none were relevant.

Surfacing the status code — e.g. Code execution request rejected (HTTP 401) — would point at the transport instead. buildCodeApiHttpErrorMessage already has response.status in hand.

Third issue: mint failures are undiagnosable

resolveCodeApiAuthHeaders discards the original error:

if (typeof authHeaders === 'function') try {
  return await authHeaders();
} catch {
  throw new CodeApiRequestError(CODE_API_AUTHORIZATION_ERROR_MESSAGE);  // original error lost
}

mintCodeApiToken can throw for several distinct reasons — "Code API JWT signing key is not configured", "Unsupported Code API JWT algorithm", "Code API JWT auth requires tenant context", "Code API token minting requires an authenticated user", "Code API token minting requires a canonical user id" — and every one of them surfaces as the same generic authorization message, with nothing logged.

In my case the mint was not failing — I established that only by calling getCodeApiAuthHeaders directly from node -e to bypass this catch. Until then I had no way to distinguish "minting threw" from "the interpreter rejected the token", because both produce the identical message with nothing logged. Logging the caught error at debug level would make JWT misconfiguration self-diagnosing.

Undocumented configuration surface

For anyone else hitting this, the JWT path reads (all from getSigningConfig, isCodeApiJwtAuthEnabled, and resolveSingleTenantId):

Variable Notes
CODEAPI_AUTH_PROVIDER librechat-jwt or both
CODEAPI_JWT_ENABLED alternative toggle
CODEAPI_JWT_PRIVATE_KEY PEM; \n escapes are normalised
CODEAPI_JWT_PRIVATE_KEY_BASE64 base64-encoded PEM
CODEAPI_JWT_PRIVATE_JWK_JSON JWK JSON
CODEAPI_JWT_ALGORITHM EdDSA (default) or RS256; anything else throws
CODEAPI_JWT_KID / CODEAPI_JWT_KEY_ID defaults to DEFAULT_KID
CODEAPI_JWT_ISSUER defaults to DEFAULT_ISSUER
CODEAPI_JWT_AUDIENCE defaults to DEFAULT_AUDIENCE
CODEAPI_JWT_TTL_SECONDS capped
CODEAPI_JWT_MINT_CACHE_SECONDS capped
CODEAPI_JWT_SINGLE_TENANT_ID defaults to DEFAULT_SINGLE_TENANT_ID

None of these appear in .env.example or the Code Interpreter documentation. Documenting them — along with the claim structure interpreters must verify — would let third-party codeapi implementations support the new scheme.

Workaround

For anyone blocked by this, a header-rewriting proxy in front of the interpreter restores the documented behaviour. Caddy, on the interpreter host:

:8080 {
	reverse_proxy localhost:8000 {
		header_up -Authorization
		header_up x-api-key "<your key>"
	}
}

Point LIBRECHAT_CODE_BASEURL at :8080. Verified working end to end — multi-file upload, execution, file generation, and inline rendering all function normally through it.

Version information

  • LibreChat v0.8.7, registry.librechat.ai/danny-avila/librechat-dev:latest
  • Deployment: Docker Compose
  • Interpreter: usnavy13/LibreCodeInterpreter v1.2.0, a third-party codeapi-compatible implementation authenticating via x-api-key
  • Model: gpt-5.6-terra, Responses API enabled on the agent

What was ruled out first

Check Result
Env vars present in the container docker compose exec api node -e "console.log(process.env.LIBRECHAT_CODE_BASEURL)" → correct URL; key also present
Network from inside the api container fetch() to the interpreter's /health → healthy
RUN_CODE permission { USE: true } on both ADMIN and USER
interface: runCode: true present in the parsed startup config
Agent capability Run Code enabled and saved
Responses API enabled per-agent; the separate 400 Function tools with reasoning_effort… error resolved by it
Interpreter with a correct key curl -H "x-api-key: $KEY" .../exec -d '{"lang":"py","code":"print(2+2)"}' → {"stdout":"4\n"}
Interpreter without a key {"error":"API key is required"} — reproduces the failure exactly

Relevant log output

error: Error calling tool Execution error:
Code execution is not authorized. Verify access before trying again.
  {"userId":"...","request_path":"/api/agents"}

error: [ON_TOOL_EXECUTE] Tool bash_tool error: Execution error:
Code execution is not authorized. Verify access before trying again.

Possibly related

Notes

The interpreter side is healthy and not in question — direct curl with x-api-key executes Python, writes to /mnt/data, and returns registered file objects. The only failure is the missing auth header on LibreChat's outbound request.

Happy to test a patch or provide further logs.


On the companion issue: I filed both sides in parallel rather than picking one, because either change alone restores interoperability and it isn't my call which project should absorb it. If the intent is that x-api-key is deprecated and JWT is the path forward, say so on either thread and I'll carry that back — the interpreter maintainer would then have a clear signal to implement verification, and the docs update becomes the fix on this side.


Appendix: full configuration

All secrets replaced with <REDACTED>; variable names preserved.

LibreChat — librechat.yaml

version: 1.2.8
cache: true
interface:
  runCode: true
endpoints:
  agents:
    capabilities: ["execute_code", "file_search", "artifacts", "tools", "actions", "context", "ocr"]
  openAI:
    models:
      default: ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"]
      fetch: false
    titleModel: "gpt-5.6-luna"
modelSpecs:
  enforce: false
  list:
    - name: "gpt-5-6-terra"
      label: "GPT-5.6 Terra"
      softDefault: true
      preset:
        endpoint: "openAI"
        model: "gpt-5.6-terra"
        useResponsesApi: true
    - name: "gpt-5-6-sol"
      label: "GPT-5.6 Sol"
      preset:
        endpoint: "openAI"
        model: "gpt-5.6-sol"
        useResponsesApi: true
    - name: "gpt-5-6-luna"
      label: "GPT-5.6 Luna"
      preset:
        endpoint: "openAI"
        model: "gpt-5.6-luna"
        useResponsesApi: true

LibreChat — docker-compose.override.yml

services:
  api:
    volumes:
    - type: bind
      source: ./librechat.yaml
      target: /app/librechat.yaml
    environment:
      - LIBRECHAT_CODE_BASEURL=${LIBRECHAT_CODE_BASEURL}
      - LIBRECHAT_CODE_API_KEY=${LIBRECHAT_CODE_API_KEY}
      - CODEAPI_AUTH_PROVIDER=${CODEAPI_AUTH_PROVIDER}
      - CODEAPI_JWT_PRIVATE_KEY=${CODEAPI_JWT_PRIVATE_KEY}
      - CODEAPI_JWT_SINGLE_TENANT_ID=${CODEAPI_JWT_SINGLE_TENANT_ID}
  meilisearch:
    profiles:
      - donotstart
  admin-panel:
    ports:
      - "3001:3000"
    environment:
      - SESSION_SECRET=${SESSION_SECRET}

The explicit environment: block is required — LibreChat's base docker-compose.yml passes an allow-list to the api container, and LIBRECHAT_CODE_* / CODEAPI_* are not on it. Without this they are silently dropped and process.env.LIBRECHAT_CODE_BASEURL is undefined inside the container despite being correct in .env.

LibreChat — .env (relevant lines)

SEARCH=false
ALLOW_REGISTRATION=true
OPENAI_API_KEY=<REDACTED>
UID=1000
GID=1000

LIBRECHAT_CODE_BASEURL=http://10.250.236.53:8080     # 8080 = auth shim; 8000 = interpreter
LIBRECHAT_CODE_API_KEY=<REDACTED>

CODEAPI_AUTH_PROVIDER=librechat-jwt
CODEAPI_JWT_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n<REDACTED ed25519>\n-----END PRIVATE KEY-----\n"
CODEAPI_JWT_SINGLE_TENANT_ID=default

CREDS_KEY=<REDACTED>
CREDS_IV=<REDACTED>
JWT_SECRET=<REDACTED>
JWT_REFRESH_SECRET=<REDACTED>
SESSION_SECRET=<REDACTED>

Note: this is the current working state, so LIBRECHAT_CODE_BASEURL points at the shim on :8080. To reproduce the bug, point it at the interpreter directly on :8000.

Both auth paths are configured simultaneously. LIBRECHAT_CODE_API_KEY is present and correct; it is simply never sent. The CODEAPI_* variables are also set and minting works — the token is just unverifiable by the interpreter.

Interpreter — .env

API_KEY=<REDACTED>
REDIS_HOST=localhost
REDIS_PORT=6379
S3_ENDPOINT=localhost:3900
S3_ACCESS_KEY=<REDACTED>
S3_SECRET_KEY=<REDACTED>

The entire auth surface is API_KEY. There is no issuer, audience, algorithm, or public-key setting — nothing a JWT could be verified against.

Interpreter — docker-compose.yml

services:
  api:
    image: ${API_IMAGE:-ghcr.io/usnavy13/librecodeinterpreter:main}
    pull_policy: always
    container_name: code-interpreter-api
    restart: unless-stopped
    init: true
    cap_add:
      - SYS_ADMIN
      - NET_ADMIN
    security_opt:
      - apparmor:unconfined
    ports:
      - "${PORT:-8000}:8000"
    env_file:
      - .env
    environment:
      - REDIS_HOST=redis
      - S3_ENDPOINT=garage:3900
    volumes:
      - sandbox-data:/var/lib/code-interpreter/sandboxes
      - skill-deps:/opt/skill-deps
      - ${SSL_CERTS_PATH:-./ssl}:/app/ssl:ro
    tmpfs:
      - /tmp:size=512m,mode=1777,noexec,nosuid,nodev
      - /app/data:size=100m
    depends_on:
      redis: { condition: service_healthy }
      garage: { condition: service_healthy }
    healthcheck:
      test: ["CMD-SHELL", "curl -fs http://localhost:8000/health || curl -fsk https://localhost:8000/health"]
      interval: 30s
      timeout: 15s
      retries: 3
      start_period: 30s

  redis:
    image: redis:7-alpine
    ports: ["127.0.0.1:${REDIS_PORT:-6379}:6379"]
    # …appendonly, 256mb maxmemory, healthcheck

  garage:
    image: dxflrs/garage:v2.3.0
    command: /garage server --single-node --default-bucket
    ports:
      - "127.0.0.1:${S3_PORT:-3900}:3900"
      - "127.0.0.1:${GARAGE_ADMIN_PORT:-3903}:3903"
    environment:
      GARAGE_DEFAULT_ACCESS_KEY: ${S3_ACCESS_KEY:-<REDACTED>}
      GARAGE_DEFAULT_SECRET_KEY: ${S3_SECRET_KEY:-<REDACTED>}
      GARAGE_DEFAULT_BUCKET: ${S3_BUCKET:-code-interpreter-files}

Only the API binds 0.0.0.0; Redis and Garage are loopback-only. No JWT configuration exists anywhere in the stack.

The auth shim — Caddyfile

Running in the same VM, on port 8080:

:8080 {
	reverse_proxy localhost:8000 {
		header_up -Authorization
		header_up x-api-key "<REDACTED>"
	}
}
docker run -d --name codeshim --network host \
  -v ~/codeshim/Caddyfile:/etc/caddy/Caddyfile:ro \
  --restart unless-stopped caddy:2-alpine

Verification that it is the header, and only the header:

# direct to the interpreter, with a Bearer token → rejected
curl -i -X POST http://10.250.236.53:8000/exec -H 'Content-Type: application/json' \
  -H "Authorization: Bearer <real LibreChat-minted JWT>" \
  -d '{"lang":"py","code":"print(1)"}'
# HTTP/1.1 401 Unauthorized
# {"error":"API key is required"}

# through the shim, with a deliberately invalid Bearer token → accepted
curl -i -X POST http://10.250.236.53:8080/exec -H 'Content-Type: application/json' \
  -H "Authorization: Bearer garbage" \
  -d '{"lang":"py","code":"print(1)"}'
# HTTP/1.1 200 OK

With LIBRECHAT_CODE_BASEURL pointed at :8080, everything works — agent-driven execution, multi-file upload, file generation, and inline rendering.

Version Information

LibreChat version: v0.8.7
Commit: d89b11d
Branch: main
Build date: 2026-08-11 03:14:37 UTC
User agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 OPR/134.0.0.0

Steps to Reproduce

Full detail in the main report, to reproduce, and verify the fix
shim workaround verified to work.

What browsers are you seeing the problem on?

Chrome

Relevant log output

I can provide as much as you want but I think the main report is complete and exhaustive

Screenshots

screenshots of multi-file upload and code execution working after shim
model is 3.6 terra. not shown in "select a model" because agent toolman selects one.
running as agent "toolman" here

Image

failure before hand would either not show the "run code" choice because the llm was emulating the print. or there would be a red popup saying it was unable to execute code.

be sure you are always using and agent with the "run code" tool. I disabled web search so only one tool in use ..run code..just to avoid questions

looks like i have it back on for toolman. so two tools may no longer cause an issue (it was a reported issue in the past)

here's the setup for agent toolman. I used the green "select" on a conv to get the agent but you can also go in the agent marketplace and click a big toolman and get a conv

Image

Code of Conduct

  • I agree to follow this project's Code of Conduct

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    🐛 bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions