mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-10-07 15:58:03 +08:00
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:
committed by
GitHub
co-authored by
openhands
parent
cb831b2860
commit
e2dd1b5f17
@@ -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",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
@@ -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}}"
|
||||
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user