fix: unify session and automation API keys into a single credential with consistent header (#681)

* fix: unify session and automation API keys into a single credential

Both the agent-server and automation backend now share the same API key
value. The agent-server validates it via `X-Session-API-Key` and the
automation backend validates it via `Authorization: Bearer …` — different
header formats, same credential.

Changes:
- Frontend: automation axios client reads `VITE_SESSION_API_KEY` instead
  of the now-removed `VITE_AUTOMATION_API_KEY`
- Dev launcher: removed separate `AUTOMATION_LOCAL_API_KEY` generation
  and persistence (`automation-api-key.txt`); `localApiKey` is set to
  `sessionApiKey` so both backends receive the same value
- Static build: stopped baking `VITE_AUTOMATION_API_KEY` (the frontend
  reads from `VITE_SESSION_API_KEY`)
- Docker entrypoint: `OPENHANDS_AUTOMATION_API_KEY`,
  `AUTOMATION_LOCAL_API_KEY`, and `AUTOMATION_AGENT_SERVER_API_KEY` all
  default to the session key when not explicitly overridden
- Tests updated to verify unified key behavior

Fixes the 401 on `/api/automation/v1` when the automation backend is
running but no separate `VITE_AUTOMATION_API_KEY` was configured.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: use X-Session-API-Key header for automation backend auth (consistent with agent-server)

Switch automation backend requests from `Authorization: Bearer …` to
`X-Session-API-Key` header, matching the agent-server's auth pattern.
Both backends now authenticate using the same header and the same key
value (`VITE_SESSION_API_KEY`).

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: address review — remove localApiKey alias, dead constant, add entrypoint guard

- Remove `localApiKey` from config; all call sites now use
  `config.sessionApiKey` directly, making the unified-key intent obvious.
- Delete `DEFAULT_AUTOMATION_API_KEY_PATH` constant and its export
  (no downstream consumers in beta).
- Add fail-fast guard in docker/entrypoint.sh when no session key is
  available, instead of silently exporting empty strings.

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: update stale comment on AUTOMATION_LOCAL_API_KEY to reflect unified session key

Co-authored-by: openhands <openhands@all-hands.dev>

---------

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
Rohit Malhotra
2026-05-26 22:15:27 +00:00
committed by GitHub
co-authored by openhands
parent cb831b2860
commit e2dd1b5f17
7 changed files with 57 additions and 97 deletions
+5 -5
View File
@@ -3,19 +3,19 @@ import { describe, expect, it } from "vitest";
import { buildAutomationBackendEnv } from "../../scripts/dev-static.mjs";
describe("dev-static", () => {
it("passes the agent-server session key to the automation backend", () => {
it("uses the same session key for both agent-server and automation backend auth", () => {
const env = buildAutomationBackendEnv({
agentServerPort: 18000,
ingressPort: 8000,
localApiKey: "automation-local-key",
sessionApiKey: "agent-session-key",
sessionApiKey: "shared-session-key",
stateDir: "/tmp/agent-canvas-state",
});
// Both backends receive the same key value
expect(env).toMatchObject({
AUTOMATION_AGENT_SERVER_URL: "http://localhost:18000",
AUTOMATION_AGENT_SERVER_API_KEY: "agent-session-key",
AUTOMATION_LOCAL_API_KEY: "automation-local-key",
AUTOMATION_AGENT_SERVER_API_KEY: "shared-session-key",
AUTOMATION_LOCAL_API_KEY: "shared-session-key",
});
});
});
+5 -30
View File
@@ -117,11 +117,11 @@ describe("buildAutomationCommand", () => {
});
describe("buildAgentServerAutomationEnv", () => {
it("exposes the local automation API key under the name agents use in curl commands", () => {
it("exposes the session API key as OPENHANDS_AUTOMATION_API_KEY for agent curl commands", () => {
expect(
buildAgentServerAutomationEnv({ localApiKey: "automation-local-key" }),
buildAgentServerAutomationEnv({ sessionApiKey: "shared-session-key" }),
).toEqual({
OPENHANDS_AUTOMATION_API_KEY: "automation-local-key",
OPENHANDS_AUTOMATION_API_KEY: "shared-session-key",
});
});
});
@@ -153,7 +153,6 @@ describe("buildConfig", () => {
keyDirs.push(dir);
return {
OH_SESSION_API_KEY_PATH: path.join(dir, "session-api-key.txt"),
OH_AUTOMATION_API_KEY_PATH: path.join(dir, "automation-api-key.txt"),
...extra,
};
}
@@ -288,33 +287,10 @@ describe("buildConfig", () => {
expect(config.verbose).toBe(true);
});
it("uses a persisted generated local automation API key by default", async () => {
it("sessionApiKey is a 64-char hex string by default", async () => {
const config = await buildConfig({}, envWithIsolatedKeyPath());
// Default is a 64-char hex string (256-bit random key)
expect(config.localApiKey).toMatch(/^[0-9a-f]{64}$/);
});
it("reuses the persisted local automation API key across restarts", async () => {
const env = envWithIsolatedKeyPath();
const first = await buildConfig({}, env);
// Simulate a fresh process invocation (the file on disk should be
// what makes the key stable).
resetPersistedSessionApiKeyCache();
const second = await buildConfig({}, env);
expect(second.localApiKey).toBe(first.localApiKey);
});
it("respects custom AUTOMATION_LOCAL_API_KEY from env", async () => {
const config = await buildConfig(
{},
envWithIsolatedKeyPath({ AUTOMATION_LOCAL_API_KEY: "my-custom-key" }),
);
expect(config.localApiKey).toBe("my-custom-key");
expect(config.sessionApiKey).toMatch(/^[0-9a-f]{64}$/);
});
it("falls back to a freshly persisted session API key by default", async () => {
@@ -451,7 +427,6 @@ describe("dev-with-automation CLI", () => {
expect(output).toContain("--dynamic");
expect(output).toContain("OH_AUTOMATION_GIT_REF");
expect(output).toContain("OH_AGENT_SERVER_LOCAL_PATH");
expect(output).toContain("AUTOMATION_LOCAL_API_KEY");
expect(output).toContain("OPENHANDS_AUTOMATION_API_KEY");
expect(output).toContain("SECRETS:");
});
+15 -1
View File
@@ -15,7 +15,9 @@
# AUTOMATION_PORT – Internal automation port (default: 18001)
# OH_SECRET_KEY – Secret key for settings encryption (auto-generated
# and persisted if not provided)
# OPENHANDS_AUTOMATION_API_KEY – API key for automation backend auth
# OPENHANDS_AUTOMATION_API_KEY – Override automation backend auth key
# (defaults to session API key — both backends
# use the same `X-Session-API-Key` header)
# Any agent-server or automation env vars are passed through.
# ═══════════════════════════════════════════════════════════════════════════════
set -uo pipefail
@@ -76,6 +78,18 @@ if [ -z "${OH_SESSION_API_KEYS_0:-}" ] && [ -z "${SESSION_API_KEY:-}" ]; then
export OH_SESSION_API_KEYS_0="$SESSION_API_KEY"
fi
# Both backends share the same API key value and the same `X-Session-API-Key`
# header for authentication. Default OPENHANDS_AUTOMATION_API_KEY to the
# session key so a single credential secures the whole stack.
EFFECTIVE_SESSION_KEY="${OH_SESSION_API_KEYS_0:-${SESSION_API_KEY:-}}"
if [ -z "$EFFECTIVE_SESSION_KEY" ]; then
log "ERROR: No session API key available — cannot configure automation auth"
exit 1
fi
export OPENHANDS_AUTOMATION_API_KEY="${OPENHANDS_AUTOMATION_API_KEY:-${EFFECTIVE_SESSION_KEY}}"
export AUTOMATION_LOCAL_API_KEY="${AUTOMATION_LOCAL_API_KEY:-${EFFECTIVE_SESSION_KEY}}"
export AUTOMATION_AGENT_SERVER_API_KEY="${AUTOMATION_AGENT_SERVER_API_KEY:-${EFFECTIVE_SESSION_KEY}}"
# AGENT_SERVER_URL — needed by automation sandbox callbacks.
export AGENT_SERVER_URL="${AGENT_SERVER_URL:-http://127.0.0.1:${AGENT_SERVER_PORT}}"
+2 -1
View File
@@ -324,13 +324,14 @@ function startAgentServer(config) {
}
function buildAutomationBackendEnv(config) {
// Both backends share the same session API key value.
return {
AUTOMATION_AGENT_SERVER_URL: `http://localhost:${config.agentServerPort}`,
AUTOMATION_AGENT_SERVER_API_KEY: config.sessionApiKey,
AUTOMATION_DB_URL: `sqlite+aiosqlite:///${join(config.stateDir, "automations.db")}`,
AUTOMATION_BASE_URL: `http://localhost:${config.ingressPort}`,
AUTOMATION_WORKSPACE_BASE: join(config.stateDir, "workspaces"),
AUTOMATION_LOCAL_API_KEY: config.localApiKey,
AUTOMATION_LOCAL_API_KEY: config.sessionApiKey,
AUTOMATION_CORS_ORIGINS: `http://localhost:${config.ingressPort},http://127.0.0.1:${config.ingressPort},http://localhost:3001,http://127.0.0.1:3001`,
FILE_STORE: "local",
LOCAL_STORAGE_PATH: join(config.stateDir, "storage"),
+24 -49
View File
@@ -35,12 +35,11 @@
* openhands-tools and openhands-workspace as editable so source edits are
* picked up without manual reinstall.
* - OH_AGENT_SERVER_GIT_REF: Git ref for agent-server
* - AUTOMATION_LOCAL_API_KEY: Custom API key for automation backend auth
* - OH_AUTOMATION_API_KEY_PATH: Override persisted default automation key path
*
* Secrets:
* The automation API key is automatically seeded into agent-server secrets
* The session API key is automatically seeded into agent-server secrets
* as OPENHANDS_AUTOMATION_API_KEY, making it available to agents in conversations.
* Both the agent-server and automation backend use the same key value
* and the same `X-Session-API-Key` header for authentication.
*/
import { spawn, spawnSync } from "node:child_process";
@@ -86,15 +85,6 @@ const DEFAULT_AUTOMATION_VERSION = SHARED_DEFAULTS.versions.automation;
const DEFAULT_AUTOMATION_SDK_VERSION = SHARED_DEFAULTS.versions.automationSdk;
const DEFAULT_BACKEND_PORT = SHARED_DEFAULTS.ports.agentServer;
const DEFAULT_AUTOMATION_PORT = SHARED_DEFAULTS.ports.automation;
// Where the auto-generated default automation API key is persisted. Static
// frontend builds bake VITE_AUTOMATION_API_KEY at build time, so the default
// must remain stable across restarts and --skip-build reuse.
const DEFAULT_AUTOMATION_API_KEY_PATH = join(
homedir(),
".openhands",
"agent-canvas",
"automation-api-key.txt",
);
// ═══════════════════════════════════════════════════════════════════════════
// Terminal Styling
@@ -213,12 +203,11 @@ ENVIRONMENT VARIABLES:
OH_AGENT_SERVER_GIT_REF Git ref for agent-server SDK (overrides default version)
OH_AGENT_SERVER_VERSION Specific PyPI version for agent-server
OH_SECRET_KEY Secret key for sessions
AUTOMATION_LOCAL_API_KEY Custom API key for automation backend auth
OH_AUTOMATION_API_KEY_PATH Override persisted default automation key path
SECRETS:
The automation API key is automatically seeded into agent-server secrets
The session API key is automatically seeded into agent-server secrets
as OPENHANDS_AUTOMATION_API_KEY, making it available to agents in conversations.
Both backends (agent-server and automation) share the same key value.
ACCESS POINTS:
Main UI: http://localhost:PORT/
@@ -339,17 +328,8 @@ async function buildConfig(args, env = process.env) {
const vscodePort = ports.backend + 1000;
// Local API key for automation backend auth. Keep the generated default
// stable across restarts because static frontend builds bake this value.
const automationApiKeyPath =
env.OH_AUTOMATION_API_KEY_PATH || DEFAULT_AUTOMATION_API_KEY_PATH;
const localApiKey =
env.AUTOMATION_LOCAL_API_KEY ||
getOrCreatePersistedApiKey(automationApiKeyPath, "automation");
// Session API key for agent-server auth
// Build a preliminary safe config to get the auto-generated session key
// This ensures both agent-server and frontend use the same key
// Session API key — shared by both agent-server and automation backend.
// Both validate it via the `X-Session-API-Key` header.
const stateDir = join(homedir(), ".openhands", "agent-canvas");
const safeConfig = buildSafeDevConfig(projectRoot, {
...env,
@@ -375,8 +355,7 @@ async function buildConfig(args, env = process.env) {
// Data directories (same as dev-safe.mjs)
stateDir,
// Auth
localApiKey,
// Auth — single key for both backends
sessionApiKey,
verbose: args.verbose,
@@ -530,12 +509,13 @@ async function waitForService(name, url, timeoutMs = 30000) {
function buildAgentServerAutomationEnv(config) {
return {
// Make the local automation backend key available to terminal commands
// spawned by the agent-server. The launcher also seeds this into Settings
// > Secrets, but agents commonly create automations with a curl command
// that references `$OPENHANDS_AUTOMATION_API_KEY`; exposing it here keeps
// that path working even before/without secret-registry env expansion.
OPENHANDS_AUTOMATION_API_KEY: config.localApiKey,
// Make the session API key available to terminal commands spawned by the
// agent-server as OPENHANDS_AUTOMATION_API_KEY. The launcher also seeds
// this into Settings > Secrets, but agents commonly create automations
// with a curl command that references `$OPENHANDS_AUTOMATION_API_KEY`;
// exposing it here keeps that path working even before/without
// secret-registry env expansion.
OPENHANDS_AUTOMATION_API_KEY: config.sessionApiKey,
};
}
@@ -651,8 +631,8 @@ function startAutomationBackend(config) {
process.env.AUTOMATION_WORKSPACE_BASE ||
config.automationWorkspaceBase ||
join(config.stateDir, "workspaces"),
// Local API key for self-hosted auth (no cloud API needed)
AUTOMATION_LOCAL_API_KEY: config.localApiKey,
// Session API key for self-hosted auth — shared with agent-server via X-Session-API-Key header
AUTOMATION_LOCAL_API_KEY: config.sessionApiKey,
// CORS: allow localhost origins for dev
AUTOMATION_CORS_ORIGINS: `http://localhost:${config.ingressPort},http://127.0.0.1:${config.ingressPort},http://localhost:3001,http://127.0.0.1:3001`,
FILE_STORE: "local",
@@ -775,29 +755,25 @@ function startVite(config) {
VITE_WORKING_DIR:
config.viteWorkingDir ?? join(config.stateDir, "workspaces"),
VITE_FRONTEND_PORT: config.vitePort.toString(),
// Session API key for frontend to authenticate with agent-server
// Session API key — used by the frontend for both agent-server and
// automation auth via the `X-Session-API-Key` header.
VITE_SESSION_API_KEY: config.sessionApiKey,
// Automation API key for frontend to authenticate with automation backend
VITE_AUTOMATION_API_KEY: config.localApiKey,
// Inform the frontend (and downstream, the agent's system prompt) about
// which services are available in this dev stack.
VITE_RUNTIME_SERVICES_INFO: JSON.stringify(runtimeServicesInfo),
// Session API key for agent-server auth (when SESSION_API_KEY is set)
...(config.sessionApiKey && {
VITE_SESSION_API_KEY: config.sessionApiKey,
}),
},
color: c.magenta,
});
}
/**
* Seed the automation API key into agent-server's secrets store.
* This makes the key available to agents during conversations.
* Seed the session API key into agent-server's secrets store as
* OPENHANDS_AUTOMATION_API_KEY so agents can authenticate with the
* automation backend in curl commands during conversations.
*
* Includes retry logic to handle slow server startup or transient failures.
*
* @param {object} config - Configuration object with agentServerPort, localApiKey, sessionApiKey
* @param {object} config - Configuration object with agentServerPort, sessionApiKey
* @param {object} options - Options for retry behavior
* @param {number} options.maxRetries - Maximum number of retry attempts (default: 5)
* @param {number} options.retryDelayMs - Delay between retries in ms (default: 2000)
@@ -816,7 +792,7 @@ async function seedAutomationSecret(config, options = {}) {
const url = `http://localhost:${config.agentServerPort}/api/settings/secrets`;
const body = JSON.stringify({
name: secretName,
value: config.localApiKey,
value: config.sessionApiKey,
description: secretDescription,
});
@@ -1137,7 +1113,6 @@ export {
DEFAULT_AUTOMATION_SDK_VERSION,
DEFAULT_BACKEND_PORT,
DEFAULT_AUTOMATION_PORT,
DEFAULT_AUTOMATION_API_KEY_PATH,
};
// ═══════════════════════════════════════════════════════════════════════════
+2 -6
View File
@@ -49,12 +49,8 @@ export function buildFrontend(config, args = {}) {
// to Vite.
VITE_WORKING_DIR:
config.viteWorkingDir ?? join(config.stateDir, "workspaces"),
// Bake the automation backend API key so the static frontend can talk
// to /api/automation through the ingress.
VITE_AUTOMATION_API_KEY: config.localApiKey,
// Bake the same session key the agent-server accepts. Without this,
// a fresh browser session seeds the Local backend with an empty key and
// all authenticated agent-server calls fail with 401.
// Bake the session API key — used by the frontend for both agent-server
// and automation auth via the `X-Session-API-Key` header.
VITE_SESSION_API_KEY: config.sessionApiKey,
// Bake a description of the runtime services in this dev stack so the
// frontend can populate the agent's <RUNTIME_SERVICES> system-prompt
@@ -20,9 +20,8 @@ export interface AutomationHealthResponse {
// Local automation calls go to the automation sidecar that
// `scripts/dev-with-automation.mjs` mounts behind the local agent-server.
// That sidecar authenticates via its own `VITE_AUTOMATION_API_KEY` Bearer
// token — NOT the agent-server's `X-Session-API-Key` — so we cannot reuse
// the default local agent-server client for these calls.
// Both backends use the same session API key (`VITE_SESSION_API_KEY`)
// and the same `X-Session-API-Key` header for consistency.
const localAutomationAxios = axios.create();
localAutomationAxios.interceptors.request.use((config) => {
@@ -33,9 +32,9 @@ localAutomationAxios.interceptors.request.use((config) => {
// eslint-disable-next-line no-param-reassign
if (!config.baseURL) config.baseURL = getEffectiveLocalBackend().host;
const apiKey = import.meta.env.VITE_AUTOMATION_API_KEY?.trim();
const apiKey = import.meta.env.VITE_SESSION_API_KEY?.trim();
if (apiKey) {
config.headers.set("Authorization", `Bearer ${apiKey}`);
config.headers.set("X-Session-API-Key", apiKey);
}
return config;
});