mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-10-07 15:28:34 +08:00
* fix: populate <RUNTIME_SERVICES> in static builds so automations work The agent's <RUNTIME_SERVICES> system-prompt block is built from VITE_RUNTIME_SERVICES_INFO, which the dev launchers set at build time. Static builds (the Docker image and the published binary) run `npm run build` without it, so the block is dropped and the agent does not know how to reach the local automation backend — it falls back to the cloud API (app.all-hands.dev) and automation creation fails. Inject the info at serve time, mirroring the existing --session-api-key path: - scripts/static-server.mjs: new --runtime-services-info flag injects the JSON as window.__AGENT_CANVAS_RUNTIME_SERVICES_INFO__ - src/api/agent-server-adapter.ts: parseRuntimeServicesInfo() falls back to that window global when the env var is empty - docker/entrypoint.sh: builds the JSON from the sandbox-facing service URLs (AGENT_SERVER_URL, AUTOMATION_BASE_URL) and passes it to the static server(s) Refs #1098 * refactor: extract runtime-services-info into a shared module Replace the inline JSON building in docker/entrypoint.sh with a single source of truth for the <RUNTIME_SERVICES> shape. buildRuntimeServicesInfo now lives in scripts/runtime-services-info.mjs (moved out of dev-safe.mjs, which re-exports it for back-compat) and gains: - optional full-URL overrides (agentServerUrl, automation.url) so the container can pass its runtime-resolved AGENT_SERVER_URL / AUTOMATION_BASE_URL and use 127.0.0.1 (avoiding IPv6 loopback) instead of build-time ports, and - a CLI entrypoint so docker/entrypoint.sh emits the JSON by running the same builder the dev stack uses, rather than a hand-rolled node -e blob. The Dockerfile ships the new dependency-free module into the image. * fix: pass --runtime-services-info in static mode and verify in automation e2e - startStaticFrontend() in dev-with-automation.mjs now builds the runtime-services info JSON via buildAutomationRuntimeServicesInfo() and passes it to static-server.mjs via --runtime-services-info. Without this, the npm binary / dev:static path served pre-built frontends that never populated the agent's <RUNTIME_SERVICES> system-prompt block (the Docker entrypoint already did this). - The mock-LLM automation e2e test now verifies that the <RUNTIME_SERVICES> block is present in the system messages sent to the LLM, and that it includes the expected service entries (Agent Server, Automation backend, /api/automation). Co-authored-by: openhands <openhands@all-hands.dev> * docs: update AGENTS.md with runtime-services-info plumbing changes Co-authored-by: openhands <openhands@all-hands.dev> --------- Co-authored-by: Rohit Malhotra <rohitvinodmalhotra@gmail.com> Co-authored-by: openhands <openhands@all-hands.dev>
141 lines
4.5 KiB
TypeScript
141 lines
4.5 KiB
TypeScript
// @vitest-environment node
|
|
import { execFileSync } from "node:child_process";
|
|
import { fileURLToPath } from "node:url";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
buildRuntimeServicesInfo,
|
|
parseArgs,
|
|
} from "../../scripts/runtime-services-info.mjs";
|
|
|
|
const SCRIPT_PATH = fileURLToPath(
|
|
new URL("../../scripts/runtime-services-info.mjs", import.meta.url),
|
|
);
|
|
|
|
// buildRuntimeServicesInfo comes from a .mjs module, so tsc infers `object`;
|
|
// cast to the structural shape we assert on (mirrors dev-safe.test.ts).
|
|
interface RuntimeServicesInfoShape {
|
|
mode?: string;
|
|
agent_host_alias: string;
|
|
services: {
|
|
agent_server?: { url_from_agent: string };
|
|
automation?: {
|
|
url_from_agent: string;
|
|
api_prefix: string;
|
|
docs_url: string;
|
|
openapi_url: string;
|
|
auth_env_var: string;
|
|
};
|
|
};
|
|
}
|
|
|
|
describe("runtime-services-info.mjs", () => {
|
|
// Port-based behavior (the dev path) is covered in dev-safe.test.ts. These
|
|
// cover the URL-override path the Docker image uses.
|
|
describe("buildRuntimeServicesInfo — URL overrides", () => {
|
|
it("uses explicit agent-server and automation URLs verbatim", () => {
|
|
const info = buildRuntimeServicesInfo({
|
|
mode: "docker",
|
|
agentHostAlias: "127.0.0.1",
|
|
agentServerUrl: "http://127.0.0.1:18000",
|
|
automation: { url: "http://127.0.0.1:8000" },
|
|
}) as RuntimeServicesInfoShape;
|
|
expect(info.agent_host_alias).toBe("127.0.0.1");
|
|
expect(info.services.agent_server?.url_from_agent).toBe(
|
|
"http://127.0.0.1:18000",
|
|
);
|
|
expect(info.services.automation).toMatchObject({
|
|
url_from_agent: "http://127.0.0.1:8000",
|
|
api_prefix: "/api/automation",
|
|
docs_url: "http://127.0.0.1:8000/api/automation/docs",
|
|
openapi_url: "http://127.0.0.1:8000/api/automation/openapi.json",
|
|
auth_env_var: "OPENHANDS_AUTOMATION_API_KEY",
|
|
});
|
|
});
|
|
|
|
it("prefers an explicit agentServerUrl over agentServerPort", () => {
|
|
const info = buildRuntimeServicesInfo({
|
|
agentServerPort: 99999,
|
|
agentServerUrl: "http://127.0.0.1:18000",
|
|
}) as RuntimeServicesInfoShape;
|
|
expect(info.services.agent_server?.url_from_agent).toBe(
|
|
"http://127.0.0.1:18000",
|
|
);
|
|
});
|
|
|
|
it("omits automation when neither url nor port is provided", () => {
|
|
const info = buildRuntimeServicesInfo({
|
|
agentServerUrl: "http://127.0.0.1:18000",
|
|
automation: {},
|
|
}) as RuntimeServicesInfoShape;
|
|
expect(info.services.automation).toBeUndefined();
|
|
});
|
|
|
|
it("throws when neither port nor url is provided", () => {
|
|
expect(() => buildRuntimeServicesInfo({ mode: "x" })).toThrow(
|
|
/agentServerPort or agentServerUrl is required/,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("parseArgs", () => {
|
|
it("maps flags to builder options", () => {
|
|
const opts = parseArgs([
|
|
"--mode",
|
|
"docker",
|
|
"--agent-host-alias",
|
|
"127.0.0.1",
|
|
"--agent-server-url",
|
|
"http://127.0.0.1:18000",
|
|
"--automation-url",
|
|
"http://127.0.0.1:8000",
|
|
]);
|
|
expect(opts).toEqual({
|
|
mode: "docker",
|
|
agentHostAlias: "127.0.0.1",
|
|
agentServerUrl: "http://127.0.0.1:18000",
|
|
automation: { url: "http://127.0.0.1:8000" },
|
|
});
|
|
});
|
|
|
|
it("omits automation when no --automation-url is given", () => {
|
|
const opts = parseArgs(["--agent-server-url", "http://127.0.0.1:18000"]);
|
|
expect(opts.automation).toBeUndefined();
|
|
});
|
|
|
|
it("throws on an unknown flag", () => {
|
|
expect(() => parseArgs(["--nope"])).toThrow(/Unknown flag/);
|
|
});
|
|
});
|
|
|
|
describe("CLI", () => {
|
|
// This is the exact path docker/entrypoint.sh relies on: run the module
|
|
// directly and capture the JSON it prints to stdout.
|
|
it("emits valid <RUNTIME_SERVICES> JSON on stdout", () => {
|
|
const stdout = execFileSync(
|
|
process.execPath,
|
|
[
|
|
SCRIPT_PATH,
|
|
"--mode",
|
|
"docker",
|
|
"--agent-host-alias",
|
|
"127.0.0.1",
|
|
"--agent-server-url",
|
|
"http://127.0.0.1:18000",
|
|
"--automation-url",
|
|
"http://127.0.0.1:8000",
|
|
],
|
|
{ encoding: "utf8" },
|
|
);
|
|
const info = JSON.parse(stdout);
|
|
expect(info.mode).toBe("docker");
|
|
expect(info.services.agent_server.url_from_agent).toBe(
|
|
"http://127.0.0.1:18000",
|
|
);
|
|
expect(info.services.automation.url_from_agent).toBe(
|
|
"http://127.0.0.1:8000",
|
|
);
|
|
});
|
|
});
|
|
});
|