Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions .env.sample
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
# OpenHands Agent Server target
# These defaults assume you are manually pointing the frontend at a backend on
# 127.0.0.1:8000. The recommended local workflow is `npm run dev`, which starts
# an isolated local backend for this checkout. Use `npm run dev:frontend` only
# when you intentionally want to point at a separately managed backend.
VITE_BACKEND_HOST="127.0.0.1:8000" # Host:port used by the Vite dev proxy
VITE_BACKEND_BASE_URL="http://127.0.0.1:8000" # Base URL used by browser-side direct requests
# VITE_SESSION_API_KEY="" # Set to the same value as backend SESSION_API_KEY or OH_SESSION_API_KEYS_0 when auth is enabled
# VITE_WORKING_DIR="/workspace/project/agent-canvas" # Base dir for per-conversation working_dirs. Each conversation's working_dir is <VITE_WORKING_DIR>/<id_hex>. Defaults to <OH_CANVAS_SAFE_STATE_DIR>/workspaces, which is the sibling of the agent server's <state_dir>/conversations/ persistence dir — both share the same <id_hex> per conversation.
# `npm run dev` and `npm run dev:static` inject same-origin launcher config
# automatically. `npm run dev:frontend` and dumb static hosting start
# unconfigured; add a remote backend through the UI.
#
# If you intentionally want a frontend-only Vite server to proxy an existing
# backend through its own origin, uncomment all three values below and set the
# session key to the backend's SESSION_API_KEY / OH_SESSION_API_KEYS_0 value.
# VITE_AGENT_SERVER_TRANSPORT="same-origin"
# VITE_AGENT_SERVER_PROXY_TARGET="127.0.0.1:8000"
# VITE_SESSION_API_KEY=""
# VITE_WORKING_DIR="/workspace/project/agent-canvas" # Base dir for per-conversation working_dirs. Each conversation's working_dir is <VITE_WORKING_DIR>/<id_hex>. Defaults to workspace/project unless launcher config overrides it.
# VITE_WORKER_URLS="" # Optional comma-separated worker URLs for the Browser tab
# VITE_ENABLE_BROWSER_TOOLS="true" # Set to false to omit BrowserToolSet from new conversations
# VITE_LOAD_PUBLIC_SKILLS="false" # Set to false to disable loading public skills from https://github.com/OpenHands/extensions (on by default)
Expand Down
11 changes: 11 additions & 0 deletions .pr/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
PR-specific QA artifacts for pull request #951.

These screenshots were captured locally from clean browser profiles and
isolated temporary agent-canvas state directories.

## Launcher Paths

- `qa/launch-npm-run-dev.png` - `npm run dev`; Vite-served frontend with launcher-injected same-origin backend.
- `qa/launch-npm-run-dev-frontend.png` - `npm run dev:frontend`; Vite-served frontend with no launcher backend.
- `qa/launch-static-same-origin.png` - `npm run dev:static -- --skip-build`; static frontend served locally with runtime-injected same-origin backend.
- `qa/launch-static-no-backend.png` - `node scripts/static-server.mjs --dir build`; static frontend served without launcher backend, matching dumb static hosting.
Binary file added .pr/qa/launch-npm-run-dev-frontend.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .pr/qa/launch-npm-run-dev.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .pr/qa/launch-static-no-backend.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .pr/qa/launch-static-same-origin.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 16 additions & 15 deletions AGENTS.md

Large diffs are not rendered by default.

66 changes: 61 additions & 5 deletions __tests__/api/agent-server-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
removeStoredConversationMetadata,
setStoredConversationMetadata,
} from "#/api/conversation-metadata-store";
import { AGENT_CANVAS_RUNTIME_CONFIG_GLOBAL } from "#/api/agent-canvas-runtime-config";
import { DEFAULT_SETTINGS } from "#/services/settings";

const {
Expand All @@ -26,13 +27,15 @@ const {
name: "Local backend",
host: "http://127.0.0.1:8000",
apiKey: "session-key",
kind: "local" as const,
kind: "agent-server" as const,
})),
}));

vi.mock("#/api/agent-server-config", () => ({
getAgentServerBaseUrl: vi.fn(() => "http://127.0.0.1:8000"),
getAgentServerSessionApiKey: vi.fn(() => null),
getAgentServerTransport: vi.fn(() => "remote"),
getLauncherAgentServerSessionApiKey: vi.fn(() => null),
getAgentServerWorkingDir: mockGetAgentServerWorkingDir,
getConfiguredWorkerUrls: vi.fn(() => []),
shouldLoadPublicSkills: vi.fn(() => true),
Expand All @@ -53,7 +56,7 @@ beforeEach(() => {
name: "Local backend",
host: "http://127.0.0.1:8000",
apiKey: "session-key",
kind: "local",
kind: "agent-server",
});
});

Expand Down Expand Up @@ -172,7 +175,6 @@ describe("buildStartConversationRequest", () => {
{ name: "terminal", params: {} },
{ name: "file_editor", params: {} },
{ name: "task_tracker", params: {} },
{ name: "canvas_ui", params: {} },
]);
});

Expand Down Expand Up @@ -200,7 +202,6 @@ describe("buildStartConversationRequest", () => {
{ name: "terminal", params: {} },
{ name: "file_editor", params: {} },
{ name: "task_tracker", params: {} },
{ name: "canvas_ui", params: {} },
{ name: "task_tool_set", params: {} },
]);
});
Expand Down Expand Up @@ -448,7 +449,7 @@ describe("buildStartConversationRequest", () => {
});

describe("canvas_ui tool injection", () => {
it("always registers canvas_ui_tool in tool_module_qualnames, even when no user settings supply qualnames", () => {
it("registers canvas_ui_tool in tool_module_qualnames when the backend advertises canvas_ui", () => {
const payload = buildStartConversationRequest({
settings: DEFAULT_SETTINGS,
}) as { tool_module_qualnames: Record<string, string> };
Expand All @@ -458,6 +459,24 @@ describe("buildStartConversationRequest", () => {
});
});

it("omits canvas_ui and its module qualname when the backend does not advertise canvas_ui", () => {
mockIsAgentServerToolAvailable.mockImplementation(
(toolName: string) => toolName !== "canvas_ui",
);

const payload = buildStartConversationRequest({
settings: DEFAULT_SETTINGS,
}) as {
agent_settings: { tools: Array<{ name: string }> };
tool_module_qualnames?: Record<string, string>;
};

expect(payload.agent_settings.tools.map((tool) => tool.name)).not.toContain(
"canvas_ui",
);
expect(payload.tool_module_qualnames).toBeUndefined();
});

it("merges user-supplied tool_module_qualnames alongside canvas_ui_tool without dropping either side", () => {
const payload = buildStartConversationRequest({
settings: {
Expand Down Expand Up @@ -675,6 +694,9 @@ describe("toAppConversation", () => {
describe("buildRuntimeServicesSystemSuffix", () => {
afterEach(() => {
vi.unstubAllEnvs();
delete (window as unknown as Record<string, unknown>)[
AGENT_CANVAS_RUNTIME_CONFIG_GLOBAL
];
});

it("returns undefined when VITE_RUNTIME_SERVICES_INFO is unset", () => {
Expand All @@ -691,6 +713,40 @@ describe("buildRuntimeServicesSystemSuffix", () => {
expect(buildRuntimeServicesSystemSuffix()).toBeUndefined();
});

it("uses runtime-injected services info before build-time env", () => {
(window as unknown as Record<string, unknown>)[
AGENT_CANVAS_RUNTIME_CONFIG_GLOBAL
] = {
runtimeServicesInfo: {
mode: "agent-canvas",
services: {
agent_server: { url_from_agent: "http://localhost:18000" },
frontend: {
kind: "static",
url_from_agent: "http://localhost:3001",
},
},
},
};
vi.stubEnv(
"VITE_RUNTIME_SERVICES_INFO",
JSON.stringify({
mode: "build-time",
services: {
agent_server: { url_from_agent: "http://localhost:9999" },
},
}),
);

const suffix = buildRuntimeServicesSystemSuffix();

expect(suffix).toContain("agent-canvas");
expect(suffix).toContain("http://localhost:18000");
expect(suffix).toContain("http://localhost:3001");
expect(suffix).not.toContain("build-time");
expect(suffix).not.toContain("http://localhost:9999");
});

it("renders a <RUNTIME_SERVICES> block when an automation entry is present", () => {
vi.stubEnv(
"VITE_RUNTIME_SERVICES_INFO",
Expand Down
62 changes: 58 additions & 4 deletions __tests__/api/agent-server-compatibility-bundled-pin.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,29 @@
import { ServerClient } from "@openhands/typescript-client/clients";
import {
ServerClient,
SettingsClient,
} from "@openhands/typescript-client/clients";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

vi.mock("#/api/agent-server-config", async (importOriginal) => {
const actual =
await importOriginal<typeof import("#/api/agent-server-config")>();
return { ...actual, hasConfiguredAgentServerDefaults: () => true };
});

import {
__resetActiveStoreForTests,
setActiveSelection,
setRegisteredBackends,
} from "#/api/backend-registry/active-store";
import type { Backend } from "#/api/backend-registry/types";
import { loadAgentServerInfo } from "#/api/agent-server-compatibility";
import {
loadAgentServerInfo,
preflightAgentServerAccess,
} from "#/api/agent-server-compatibility";

const { getServerInfoMock } = vi.hoisted(() => ({
const { getServerInfoMock, getSettingsMock } = vi.hoisted(() => ({
getServerInfoMock: vi.fn(),
getSettingsMock: vi.fn(),
}));

vi.mock("@openhands/typescript-client/clients", () => ({
Expand All @@ -18,6 +32,11 @@ vi.mock("@openhands/typescript-client/clients", () => ({
getServerInfo: getServerInfoMock,
};
}),
SettingsClient: vi.fn(function SettingsClientMock() {
return {
getSettings: getSettingsMock,
};
}),
}));

const cloudBackend: Backend = {
Expand All @@ -32,16 +51,20 @@ beforeEach(() => {
window.localStorage.clear();
__resetActiveStoreForTests();
getServerInfoMock.mockReset();
getSettingsMock.mockReset();
vi.mocked(ServerClient).mockClear();
vi.mocked(SettingsClient).mockClear();
getServerInfoMock.mockResolvedValue({ version: "1.0.0" });
getSettingsMock.mockResolvedValue({});
});

afterEach(() => {
window.localStorage.clear();
__resetActiveStoreForTests();
vi.unstubAllEnvs();
});

describe("loadAgentServerInfo", () => {
describe("agent-server compatibility probes", () => {
it("targets the bundled local backend even when the active backend is cloud", async () => {
setRegisteredBackends([cloudBackend]);
setActiveSelection({ backendId: cloudBackend.id });
Expand All @@ -60,4 +83,35 @@ describe("loadAgentServerInfo", () => {
expect(overrides.host).not.toBe(cloudBackend.host);
expect(overrides.host).not.toContain("all-hands.dev");
});

it("uses launcher auth for same-origin preflight probes", async () => {
vi.stubEnv("VITE_AGENT_SERVER_TRANSPORT", "same-origin");
vi.stubEnv("VITE_SESSION_API_KEY", "launcher-session-key");
const sameOriginBackend: Backend = {
id: "same-origin-agent",
name: "Local",
host: window.location.origin,
apiKey: "stale-stored-key",
kind: "agent-server",
agentServerTransport: "same-origin",
};
setRegisteredBackends([sameOriginBackend]);

await preflightAgentServerAccess();

expect(ServerClient).toHaveBeenCalledOnce();
expect(SettingsClient).toHaveBeenCalledOnce();
expect(vi.mocked(ServerClient).mock.calls[0][0]).toEqual(
expect.objectContaining({
host: window.location.origin,
apiKey: "launcher-session-key",
}),
);
expect(vi.mocked(SettingsClient).mock.calls[0][0]).toEqual(
expect.objectContaining({
host: window.location.origin,
apiKey: "launcher-session-key",
}),
);
});
});
Loading
Loading