From 96a147abcfd619ef9d61d1c8feb102de779cfec8 Mon Sep 17 00:00:00 2001 From: neubig Date: Sun, 30 Aug 2026 02:30:51 +0000 Subject: [PATCH] fix: close remaining launcher key injection paths Co-authored-by: openhands --- README.md | 13 ++++++------- __tests__/scripts/dev-safe.test.ts | 15 +++++++++++++++ __tests__/scripts/dev-static.test.ts | 11 +++++++++++ scripts/dev-safe.mjs | 13 +++++++++++-- scripts/static-build.mjs | 25 +++++++++++++------------ 5 files changed, 56 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index f676f87b0f..f7dcc2520f 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ Access the UI at [http://localhost:8000](http://localhost:8000) for the npm/sour Local (`npx` / `npm run dev`) listeners bind **loopback only** (`127.0.0.1`) so the auto-injected session key is not reachable from other machines on the network. To listen on all interfaces, pass `--host 0.0.0.0` (or set `OH_BIND_HOST`); the session key is then **not** injected and the UI uses the same API-key entry screen as `--public`. -Docker listens on all container interfaces so port publishing works, but does not inject its session key into HTML by default. The quickstart above explicitly enables injection while publishing the host port on `127.0.0.1` only. If you publish Docker on a LAN or public interface, omit `AGENT_CANVAS_ALLOW_LAN_SESSION_KEY`, set `LOCAL_BACKEND_API_KEY` to a strong value, and enter that value in the UI. For internet-facing installs, follow [self-hosting](./docs/SELF_HOSTING.md). +Docker listens on all container interfaces so port publishing works, but does not inject its session key into HTML by default. The quickstart above explicitly enables injection while publishing the host port on `127.0.0.1` only. If you publish Docker on a LAN or public interface, omit `AGENT_CANVAS_ALLOW_LAN_SESSION_KEY` and enter the API key in the UI. Set `LOCAL_BACKEND_API_KEY` to a strong value, or retrieve the generated value with `docker exec sh -c 'cat "$STATE_DIR/api-key.txt"'`. For internet-facing installs, follow [self-hosting](./docs/SELF_HOSTING.md). # Architecture @@ -145,16 +145,15 @@ The Agent Server is often paired with an [Automation Server](https://github.com/ Agent Canvas is part of a multi-repository OpenHands system. Changes should go to the repository that owns the behavior: -| Repository | Responsibility | -|---|---| -| [`OpenHands/OpenHands`](https://github.com/OpenHands/OpenHands) | Agent Canvas frontend, user-facing control center, backend selection, and local-stack orchestration. | +| Repository | Responsibility | +| --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | +| [`OpenHands/OpenHands`](https://github.com/OpenHands/OpenHands) | Agent Canvas frontend, user-facing control center, backend selection, and local-stack orchestration. | | [`OpenHands/software-agent-sdk`](https://github.com/OpenHands/software-agent-sdk) | Python SDK, Agent Server, agents, tools, conversations, workspaces, events, and the canonical server API. | -| [`OpenHands/typescript-client`](https://github.com/OpenHands/typescript-client) | Browser-compatible TypeScript client for the Agent Server API. | -| [`OpenHands/automation`](https://github.com/OpenHands/automation) | Automation definitions, scheduling, webhooks, run history, and dispatching. | +| [`OpenHands/typescript-client`](https://github.com/OpenHands/typescript-client) | Browser-compatible TypeScript client for the Agent Server API. | +| [`OpenHands/automation`](https://github.com/OpenHands/automation) | Automation definitions, scheduling, webhooks, run history, and dispatching. | The Agent Server API is implemented by the SDK and consumed through the TypeScript client by Agent Canvas. The automation service decides when work runs and dispatches conversations to the Agent Server/SDK, which decides what runs. See [`AGENTS.md`](./AGENTS.md) for contributor-specific boundaries and the required custom code-review guide. - ## More documentation - [Documentation index](./docs/README.md) diff --git a/__tests__/scripts/dev-safe.test.ts b/__tests__/scripts/dev-safe.test.ts index a39a5eec6a..7ce8488698 100644 --- a/__tests__/scripts/dev-safe.test.ts +++ b/__tests__/scripts/dev-safe.test.ts @@ -26,6 +26,7 @@ import { formatMissingUvxGuidance, formatMissingFrontendDependenciesGuidance, getMissingFrontendDependencyBins, + getViteSessionApiKey, validateFrontendDependencies, validateLocalAgentServerPath, findFreePort, @@ -690,6 +691,20 @@ describe("validateLocalAgentServerPath", () => { }); }); +describe("getViteSessionApiKey", () => { + it("only injects the key on loopback listeners", () => { + const config = { sessionApiKey: "local-secret" }; + + expect(getViteSessionApiKey(config, {})).toBe("local-secret"); + expect(getViteSessionApiKey(config, { VITE_BIND_HOST: "127.0.0.1" })).toBe( + "local-secret", + ); + expect(getViteSessionApiKey(config, { VITE_BIND_HOST: "0.0.0.0" })).toBe( + "", + ); + }); +}); + describe("buildSafeDevConfig", () => { let keyTmp: string | null = null; diff --git a/__tests__/scripts/dev-static.test.ts b/__tests__/scripts/dev-static.test.ts index f33d8e1d62..2968bfbcff 100644 --- a/__tests__/scripts/dev-static.test.ts +++ b/__tests__/scripts/dev-static.test.ts @@ -5,6 +5,7 @@ import { buildLocalServiceRouteArgs, parseArgs, } from "../../scripts/dev-static.mjs"; +import { buildFrontendEnv } from "../../scripts/static-build.mjs"; describe("dev-static CLI", () => { it.each([ @@ -38,6 +39,16 @@ describe("dev-static", () => { }); }); + it("keeps reusable frontend builds free of session credentials", () => { + const env = buildFrontendEnv( + { viteWorkingDir: "/tmp/workspace" }, + { VITE_SESSION_API_KEY: "inherited-secret" }, + ); + + expect(env.VITE_SESSION_API_KEY).toBe(""); + expect(env.VITE_WORKING_DIR).toBe("/tmp/workspace"); + }); + it("points every local proxy route at the IPv4 loopback", () => { // Both backends bind to `0.0.0.0`, which only accepts IPv4, so a // `localhost` target strands the proxy on ::1 on Windows. diff --git a/scripts/dev-safe.mjs b/scripts/dev-safe.mjs index ad8edf967a..33821d500e 100644 --- a/scripts/dev-safe.mjs +++ b/scripts/dev-safe.mjs @@ -16,6 +16,7 @@ import process from "node:process"; import { setTimeout as delay } from "node:timers/promises"; import { fileURLToPath, pathToFileURL } from "node:url"; +import { applySessionKeyPolicy } from "./bind-host.mjs"; import { getProcessTreeSpawnOptions, isProcessRunning, @@ -576,6 +577,15 @@ export function buildSafeDevConfig(cwd = process.cwd(), env = process.env) { return buildConfigFromPorts({ backendPort, vscodePort }, cwd, env); } +export function getViteSessionApiKey(config, env = process.env) { + return ( + applySessionKeyPolicy({ + host: env.VITE_BIND_HOST || "127.0.0.1", + sessionApiKey: config.sessionApiKey, + }).sessionApiKey || "" + ); +} + /** * Build safe dev configuration with dynamic port allocation. * @@ -1088,8 +1098,7 @@ async function main() { VITE_BACKEND_HOST: config.backendHost, VITE_BACKEND_BASE_URL: config.backendBaseUrl, VITE_WORKING_DIR: config.workingDir, - // Pass session API key so frontend can authenticate with agent-server - VITE_SESSION_API_KEY: config.sessionApiKey, + VITE_SESSION_API_KEY: getViteSessionApiKey(config), // This mode has no static server or ingress in front of Vite, so Vite's // own proxy is the only thing that can serve the editor prefix on the // frontend origin. The editor is a separate process on a port of its diff --git a/scripts/static-build.mjs b/scripts/static-build.mjs index 23bb529a85..58a9cb579d 100644 --- a/scripts/static-build.mjs +++ b/scripts/static-build.mjs @@ -11,6 +11,16 @@ import { } from "./dev-with-automation.mjs"; import { buildNpmScriptCommand } from "./dev-safe.mjs"; +export function buildFrontendEnv(config, env = process.env) { + return { + ...env, + VITE_SESSION_API_KEY: "", + ...(config.viteWorkingDir + ? { VITE_WORKING_DIR: config.viteWorkingDir } + : {}), + }; +} + export function buildFrontend(config, args = {}) { const buildDir = join(config.canvasPath, "build"); @@ -39,18 +49,9 @@ export function buildFrontend(config, args = {}) { ); const cmd = buildNpmScriptCommand("build:app"); - const buildEnv = { - ...process.env, - // 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, - // Intentionally do NOT set VITE_BACKEND_BASE_URL: leaving it unset makes - // the runtime fall back to window.location.origin, which keeps the build - // portable across localhost, LAN hosts, and tunnels such as ngrok. - }; - if (config.viteWorkingDir) { - buildEnv.VITE_WORKING_DIR = config.viteWorkingDir; - } + // The static server injects runtime configuration after applying the + // bind-host policy, so the reusable build itself must remain key-free. + const buildEnv = buildFrontendEnv(config); const result = spawnSync(cmd.command, cmd.args, { cwd: config.canvasPath,