fix(browser): prefer user profile over chrome relay

This commit is contained in:
Peter Steinberger
2026-03-14 04:15:25 +00:00
parent 1f9cc647f8
commit b6d1d0d72d
21 changed files with 211 additions and 247 deletions

View File

@@ -160,9 +160,8 @@ describe("createOpenClawCodingTools", () => {
it("mentions Chrome extension relay in browser tool description", () => {
const browser = createBrowserTool();
expect(browser.description).toMatch(/Chrome extension/i);
expect(browser.description).toMatch(/browserSession="agent"/i);
expect(browser.description).toMatch(/browserSession="user"/i);
expect(browser.description).toMatch(/profile="chrome"/i);
expect(browser.description).toMatch(/profile="user"/i);
expect(browser.description).toMatch(/profile="chrome-relay"/i);
});
it("keeps browser tool schema properties after normalization", () => {
const browser = defaultTools.find((tool) => tool.name === "browser");
@@ -174,7 +173,6 @@ describe("createOpenClawCodingTools", () => {
};
expect(parameters.properties?.action).toBeDefined();
expect(parameters.properties?.target).toBeDefined();
expect(parameters.properties?.browserSession).toBeDefined();
expect(parameters.properties?.targetUrl).toBeDefined();
expect(parameters.properties?.request).toBeDefined();
expect(parameters.required ?? []).toContain("action");

View File

@@ -74,7 +74,7 @@ function formatConsoleToolResult(result: {
}
function isChromeStaleTargetError(profile: string | undefined, err: unknown): boolean {
if (profile !== "chrome") {
if (profile !== "chrome-relay" && profile !== "chrome") {
return false;
}
const msg = String(err);
@@ -340,7 +340,7 @@ export async function executeActAction(params: {
);
}
throw new Error(
`Chrome tab not found (stale targetId?). Run action=tabs profile="chrome" and use one of the returned targetIds.`,
`Chrome tab not found (stale targetId?). Run action=tabs profile="chrome-relay" and use one of the returned targetIds.`,
{ cause: err },
);
}

View File

@@ -35,7 +35,6 @@ const BROWSER_TOOL_ACTIONS = [
] as const;
const BROWSER_TARGETS = ["sandbox", "host", "node"] as const;
const BROWSER_SESSION_CHOICES = ["agent", "user"] as const;
const BROWSER_SNAPSHOT_FORMATS = ["aria", "ai"] as const;
const BROWSER_SNAPSHOT_MODES = ["efficient"] as const;
@@ -89,7 +88,6 @@ const BrowserActSchema = Type.Object({
export const BrowserToolSchema = Type.Object({
action: stringEnum(BROWSER_TOOL_ACTIONS),
target: optionalStringEnum(BROWSER_TARGETS),
browserSession: optionalStringEnum(BROWSER_SESSION_CHOICES),
node: Type.Optional(Type.String()),
profile: Type.Optional(Type.String()),
targetUrl: Type.Optional(Type.String()),

View File

@@ -187,7 +187,6 @@ async function runSnapshotToolCall(params: {
refs?: "aria" | "dom";
maxChars?: number;
profile?: string;
browserSession?: "agent" | "user";
}) {
const tool = createBrowserTool();
await tool.execute?.("call-1", { action: "snapshot", ...params });
@@ -288,58 +287,56 @@ describe("browser tool snapshot maxChars", () => {
expect(opts?.mode).toBeUndefined();
});
it("defaults to host when using profile=chrome (even in sandboxed sessions)", async () => {
const tool = createBrowserTool({ sandboxBridgeUrl: "http://127.0.0.1:9999" });
await tool.execute?.("call-1", { action: "snapshot", profile: "chrome", snapshotFormat: "ai" });
expect(browserClientMocks.browserSnapshot).toHaveBeenCalledWith(
undefined,
expect.objectContaining({
profile: "chrome",
}),
);
});
it('uses the isolated openclaw profile for browserSession="agent"', async () => {
await runSnapshotToolCall({ browserSession: "agent", snapshotFormat: "ai" });
expect(browserClientMocks.browserSnapshot).toHaveBeenCalledWith(
undefined,
expect.objectContaining({
profile: "openclaw",
}),
);
});
it('uses the host user browser for browserSession="user"', async () => {
it("defaults to host when using profile=chrome-relay (even in sandboxed sessions)", async () => {
setResolvedBrowserProfiles({
openclaw: { cdpPort: 18800, color: "#FF4500" },
chrome: { driver: "extension", cdpUrl: "http://127.0.0.1:18792", color: "#0066CC" },
"chrome-relay": {
driver: "extension",
cdpUrl: "http://127.0.0.1:18792",
color: "#0066CC",
},
});
const tool = createBrowserTool({ sandboxBridgeUrl: "http://127.0.0.1:9999" });
await tool.execute?.("call-1", {
action: "snapshot",
browserSession: "user",
profile: "chrome-relay",
snapshotFormat: "ai",
});
expect(browserClientMocks.browserSnapshot).toHaveBeenCalledWith(
undefined,
expect.objectContaining({
profile: "chrome",
profile: "chrome-relay",
}),
);
});
it('uses a sole existing-session profile for browserSession="user"', async () => {
it("defaults to host when using profile=user (even in sandboxed sessions)", async () => {
setResolvedBrowserProfiles({
user: { driver: "existing-session", attachOnly: true, color: "#00AA00" },
});
const tool = createBrowserTool({ sandboxBridgeUrl: "http://127.0.0.1:9999" });
await tool.execute?.("call-1", {
action: "snapshot",
profile: "user",
snapshotFormat: "ai",
});
expect(browserClientMocks.browserSnapshot).toHaveBeenCalledWith(
undefined,
expect.objectContaining({
profile: "user",
}),
);
});
it("defaults to host for custom existing-session profiles too", async () => {
setResolvedBrowserProfiles({
openclaw: { cdpPort: 18800, color: "#FF4500" },
"chrome-live": { driver: "existing-session", attachOnly: true, color: "#00AA00" },
});
const tool = createBrowserTool({ sandboxBridgeUrl: "http://127.0.0.1:9999" });
await tool.execute?.("call-1", {
action: "snapshot",
browserSession: "user",
profile: "chrome-live",
snapshotFormat: "ai",
});
@@ -351,47 +348,30 @@ describe("browser tool snapshot maxChars", () => {
);
});
it('fails when browserSession="user" is ambiguous', async () => {
it('rejects profile="user" with target="sandbox"', async () => {
setResolvedBrowserProfiles({
openclaw: { cdpPort: 18800, color: "#FF4500" },
personal: { driver: "existing-session", attachOnly: true, color: "#00AA00" },
work: { driver: "existing-session", attachOnly: true, color: "#0066CC" },
});
const tool = createBrowserTool();
await expect(
tool.execute?.("call-1", {
action: "snapshot",
browserSession: "user",
snapshotFormat: "ai",
}),
).rejects.toThrow(/Multiple user-browser profiles are configured/);
});
it('rejects browserSession="user" with target="sandbox"', async () => {
setResolvedBrowserProfiles({
chrome: { driver: "extension", cdpUrl: "http://127.0.0.1:18792", color: "#0066CC" },
user: { driver: "existing-session", attachOnly: true, color: "#00AA00" },
});
const tool = createBrowserTool({ sandboxBridgeUrl: "http://127.0.0.1:9999" });
await expect(
tool.execute?.("call-1", {
action: "snapshot",
browserSession: "user",
profile: "user",
target: "sandbox",
snapshotFormat: "ai",
}),
).rejects.toThrow(/cannot use the sandbox browser/);
).rejects.toThrow(/profile="user" cannot use the sandbox browser/i);
});
it("lets the server choose snapshot format when the user does not request one", async () => {
const tool = createBrowserTool();
await tool.execute?.("call-1", { action: "snapshot", profile: "chrome" });
await tool.execute?.("call-1", { action: "snapshot", profile: "chrome-relay" });
expect(browserClientMocks.browserSnapshot).toHaveBeenCalledWith(
undefined,
expect.objectContaining({
profile: "chrome",
profile: "chrome-relay",
}),
);
const opts = browserClientMocks.browserSnapshot.mock.calls.at(-1)?.[1] as
@@ -458,14 +438,21 @@ describe("browser tool snapshot maxChars", () => {
expect(gatewayMocks.callGatewayTool).not.toHaveBeenCalled();
});
it("keeps chrome profile on host when node proxy is available", async () => {
it("keeps chrome-relay profile on host when node proxy is available", async () => {
mockSingleBrowserProxyNode();
setResolvedBrowserProfiles({
"chrome-relay": {
driver: "extension",
cdpUrl: "http://127.0.0.1:18792",
color: "#0066CC",
},
});
const tool = createBrowserTool();
await tool.execute?.("call-1", { action: "status", profile: "chrome" });
await tool.execute?.("call-1", { action: "status", profile: "chrome-relay" });
expect(browserClientMocks.browserStatus).toHaveBeenCalledWith(
undefined,
expect.objectContaining({ profile: "chrome" }),
expect.objectContaining({ profile: "chrome-relay" }),
);
expect(gatewayMocks.callGatewayTool).not.toHaveBeenCalled();
});
@@ -758,7 +745,7 @@ describe("browser tool external content wrapping", () => {
describe("browser tool act stale target recovery", () => {
registerBrowserToolAfterEachReset();
it("retries safe chrome act once without targetId when exactly one tab remains", async () => {
it("retries safe chrome-relay act once without targetId when exactly one tab remains", async () => {
browserActionsMocks.browserAct
.mockRejectedValueOnce(new Error("404: tab not found"))
.mockResolvedValueOnce({ ok: true });
@@ -767,7 +754,7 @@ describe("browser tool act stale target recovery", () => {
const tool = createBrowserTool();
const result = await tool.execute?.("call-1", {
action: "act",
profile: "chrome",
profile: "chrome-relay",
request: {
kind: "hover",
targetId: "stale-tab",
@@ -780,18 +767,18 @@ describe("browser tool act stale target recovery", () => {
1,
undefined,
expect.objectContaining({ targetId: "stale-tab", kind: "hover", ref: "btn-1" }),
expect.objectContaining({ profile: "chrome" }),
expect.objectContaining({ profile: "chrome-relay" }),
);
expect(browserActionsMocks.browserAct).toHaveBeenNthCalledWith(
2,
undefined,
expect.not.objectContaining({ targetId: expect.anything() }),
expect.objectContaining({ profile: "chrome" }),
expect.objectContaining({ profile: "chrome-relay" }),
);
expect(result?.details).toMatchObject({ ok: true });
});
it("does not retry mutating chrome act requests without targetId", async () => {
it("does not retry mutating chrome-relay act requests without targetId", async () => {
browserActionsMocks.browserAct.mockRejectedValueOnce(new Error("404: tab not found"));
browserClientMocks.browserTabs.mockResolvedValueOnce([{ targetId: "only-tab" }]);
@@ -799,14 +786,14 @@ describe("browser tool act stale target recovery", () => {
await expect(
tool.execute?.("call-1", {
action: "act",
profile: "chrome",
profile: "chrome-relay",
request: {
kind: "click",
targetId: "stale-tab",
ref: "btn-1",
},
}),
).rejects.toThrow(/Run action=tabs profile="chrome"/i);
).rejects.toThrow(/Run action=tabs profile="chrome-relay"/i);
expect(browserActionsMocks.browserAct).toHaveBeenCalledTimes(1);
});

View File

@@ -17,7 +17,6 @@ import {
browserStop,
} from "../../browser/client.js";
import { resolveBrowserConfig, resolveProfile } from "../../browser/config.js";
import { DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME } from "../../browser/constants.js";
import { DEFAULT_UPLOAD_DIR, resolveExistingPathsWithinRoot } from "../../browser/paths.js";
import { getBrowserProfileCapabilities } from "../../browser/profile-capabilities.js";
import { applyBrowserProxyPaths, persistBrowserProxyFiles } from "../../browser/proxy-files.js";
@@ -280,58 +279,22 @@ function resolveBrowserBaseUrl(params: {
return undefined;
}
function listUserBrowserProfiles() {
function shouldPreferHostForProfile(profileName: string | undefined) {
if (!profileName) {
return false;
}
const cfg = loadConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
return Object.keys(resolved.profiles ?? {})
.map((name) => resolveProfile(resolved, name))
.filter((profile): profile is NonNullable<typeof profile> => Boolean(profile))
.filter((profile) => {
const capabilities = getBrowserProfileCapabilities(profile);
return capabilities.requiresRelay || capabilities.usesChromeMcp;
});
const profile = resolveProfile(resolved, profileName);
if (!profile) {
return false;
}
const capabilities = getBrowserProfileCapabilities(profile);
return capabilities.requiresRelay || capabilities.usesChromeMcp;
}
function resolveBrowserToolProfile(params: {
profile?: string;
browserSession?: "agent" | "user";
}): string | undefined {
if (params.profile) {
return params.profile;
}
if (!params.browserSession) {
return undefined;
}
if (params.browserSession === "agent") {
return DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME;
}
const userProfiles = listUserBrowserProfiles();
const defaultUserProfile = userProfiles.find(
(profile) => profile.name !== DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME,
);
if (defaultUserProfile?.name === "chrome") {
return defaultUserProfile.name;
}
const chromeRelay = userProfiles.find((profile) => profile.name === "chrome");
if (chromeRelay) {
return chromeRelay.name;
}
if (userProfiles.length === 1) {
return userProfiles[0]?.name;
}
const chromeLive = userProfiles.find((profile) => profile.name === "chrome-live");
if (chromeLive) {
return chromeLive.name;
}
if (userProfiles.length === 0) {
throw new Error(
'No user-browser profile is configured. Use profile="chrome" for the extension relay or create an existing-session profile first.',
);
}
throw new Error(
`Multiple user-browser profiles are configured (${userProfiles.map((profile) => profile.name).join(", ")}). Pass profile="<name>".`,
);
function isHostOnlyProfileName(profileName: string | undefined) {
return profileName === "user" || profileName === "chrome-relay";
}
export function createBrowserTool(opts?: {
@@ -347,12 +310,12 @@ export function createBrowserTool(opts?: {
name: "browser",
description: [
"Control the browser via OpenClaw's browser control server (status/start/stop/profiles/tabs/open/snapshot/screenshot/actions).",
'Browser choice: use browserSession="agent" by default for the isolated OpenClaw browser. Use browserSession="user" only when logged-in browser state matters and the user is present to click/approve browser attach prompts.',
'browserSession="user" means the real local user browser on the host, not sandbox/node browsers. If user presence is unclear, ask first.',
'profile remains the explicit override. Use profile="chrome" for Chrome extension relay takeover (existing Chrome tabs). Use profile="openclaw" for the isolated OpenClaw-managed browser.',
'If the user mentions the Chrome extension / Browser Relay / toolbar button / “attach tab”, ALWAYS use browserSession="user" and prefer profile="chrome" (do not ask which profile unless ambiguous).',
"Browser choice: omit profile by default for the isolated OpenClaw-managed browser (`openclaw`).",
'For the logged-in user browser on the local host, prefer profile="user". Use it only when existing logins/cookies matter and the user is present to click/approve any browser attach prompt.',
'Use profile="chrome-relay" only for the Chrome extension / Browser Relay / toolbar-button attach-tab flow, or when the user explicitly asks for the extension relay.',
'If the user mentions the Chrome extension / Browser Relay / toolbar button / “attach tab”, ALWAYS prefer profile="chrome-relay". Otherwise prefer profile="user" over the extension relay for user-browser work.',
'When a node-hosted browser proxy is available, the tool may auto-route to it. Pin a node with node=<id|name> or target="node".',
"User-browser flows need user interaction: Chrome extension relay needs the user to click the OpenClaw Browser Relay toolbar icon on the tab (badge ON); existing-session may require approving a browser attach prompt.",
'User-browser flows need user interaction: profile="user" may require approving a browser attach prompt; profile="chrome-relay" needs the user to click the OpenClaw Browser Relay toolbar icon on the tab (badge ON). If user presence is unclear, ask first.',
"When using refs from snapshot (e.g. e12), keep the same tab: prefer passing targetId from the snapshot response into subsequent actions (act/click/type/etc).",
'For stable, self-resolving refs across calls, use snapshot with refs="aria" (Playwright aria-ref ids). Default refs="role" are role+name-based.',
"Use snapshot+act for UI automation. Avoid act:wait by default; use only in exceptional cases when no reliable UI state exists.",
@@ -363,36 +326,25 @@ export function createBrowserTool(opts?: {
execute: async (_toolCallId, args) => {
const params = args as Record<string, unknown>;
const action = readStringParam(params, "action", { required: true });
const browserSession = readStringParam(params, "browserSession") as
| "agent"
| "user"
| undefined;
const profile = resolveBrowserToolProfile({
profile: readStringParam(params, "profile"),
browserSession,
});
const profile = readStringParam(params, "profile");
const requestedNode = readStringParam(params, "node");
let target = readStringParam(params, "target") as "sandbox" | "host" | "node" | undefined;
if (requestedNode && target && target !== "node") {
throw new Error('node is only supported with target="node".');
}
if (browserSession === "user") {
if (isHostOnlyProfileName(profile)) {
if (requestedNode || target === "node") {
throw new Error('browserSession="user" only supports the local host browser.');
throw new Error(`profile="${profile}" only supports the local host browser.`);
}
if (target === "sandbox") {
throw new Error(
'browserSession="user" cannot use the sandbox browser; use target="host" or omit target.',
`profile="${profile}" cannot use the sandbox browser; use target="host" or omit target.`,
);
}
}
if (!target && !requestedNode && browserSession === "user") {
target = "host";
}
if (!target && !requestedNode && profile === "chrome") {
// Chrome extension relay takeover is a host Chrome feature; prefer host unless explicitly targeting a node.
if (!target && !requestedNode && shouldPreferHostForProfile(profile)) {
// Local host user-browser profiles should not silently bind to sandbox/node browsers.
target = "host";
}

View File

@@ -22,10 +22,14 @@ describe("browser config", () => {
expect(openclaw?.driver).toBe("openclaw");
expect(openclaw?.cdpPort).toBe(18800);
expect(openclaw?.cdpUrl).toBe("http://127.0.0.1:18800");
const chrome = resolveProfile(resolved, "chrome");
expect(chrome?.driver).toBe("extension");
expect(chrome?.cdpPort).toBe(18792);
expect(chrome?.cdpUrl).toBe("http://127.0.0.1:18792");
const user = resolveProfile(resolved, "user");
expect(user?.driver).toBe("existing-session");
expect(user?.cdpPort).toBe(0);
expect(user?.cdpUrl).toBe("");
const chromeRelay = resolveProfile(resolved, "chrome-relay");
expect(chromeRelay?.driver).toBe("extension");
expect(chromeRelay?.cdpPort).toBe(18792);
expect(chromeRelay?.cdpUrl).toBe("http://127.0.0.1:18792");
expect(resolved.remoteCdpTimeoutMs).toBe(1500);
expect(resolved.remoteCdpHandshakeTimeoutMs).toBe(3000);
});
@@ -34,10 +38,10 @@ describe("browser config", () => {
withEnv({ OPENCLAW_GATEWAY_PORT: "19001" }, () => {
const resolved = resolveBrowserConfig(undefined);
expect(resolved.controlPort).toBe(19003);
const chrome = resolveProfile(resolved, "chrome");
expect(chrome?.driver).toBe("extension");
expect(chrome?.cdpPort).toBe(19004);
expect(chrome?.cdpUrl).toBe("http://127.0.0.1:19004");
const chromeRelay = resolveProfile(resolved, "chrome-relay");
expect(chromeRelay?.driver).toBe("extension");
expect(chromeRelay?.cdpPort).toBe(19004);
expect(chromeRelay?.cdpUrl).toBe("http://127.0.0.1:19004");
const openclaw = resolveProfile(resolved, "openclaw");
expect(openclaw?.cdpPort).toBe(19012);
@@ -49,10 +53,10 @@ describe("browser config", () => {
withEnv({ OPENCLAW_GATEWAY_PORT: undefined }, () => {
const resolved = resolveBrowserConfig(undefined, { gateway: { port: 19011 } });
expect(resolved.controlPort).toBe(19013);
const chrome = resolveProfile(resolved, "chrome");
expect(chrome?.driver).toBe("extension");
expect(chrome?.cdpPort).toBe(19014);
expect(chrome?.cdpUrl).toBe("http://127.0.0.1:19014");
const chromeRelay = resolveProfile(resolved, "chrome-relay");
expect(chromeRelay?.driver).toBe("extension");
expect(chromeRelay?.cdpPort).toBe(19014);
expect(chromeRelay?.cdpUrl).toBe("http://127.0.0.1:19014");
const openclaw = resolveProfile(resolved, "openclaw");
expect(openclaw?.cdpPort).toBe(19022);
@@ -205,13 +209,13 @@ describe("browser config", () => {
);
});
it("does not add the built-in chrome extension profile if the derived relay port is already used", () => {
it("does not add the built-in chrome-relay profile if the derived relay port is already used", () => {
const resolved = resolveBrowserConfig({
profiles: {
openclaw: { cdpPort: 18792, color: "#FF4500" },
},
});
expect(resolveProfile(resolved, "chrome")).toBe(null);
expect(resolveProfile(resolved, "chrome-relay")).toBe(null);
expect(resolved.defaultProfile).toBe("openclaw");
});
@@ -313,7 +317,7 @@ describe("browser config", () => {
const managed = resolveProfile(resolved, "openclaw")!;
expect(getBrowserProfileCapabilities(managed).usesChromeMcp).toBe(false);
const extension = resolveProfile(resolved, "chrome")!;
const extension = resolveProfile(resolved, "chrome-relay")!;
expect(getBrowserProfileCapabilities(extension).usesChromeMcp).toBe(false);
const work = resolveProfile(resolved, "work")!;
@@ -354,17 +358,17 @@ describe("browser config", () => {
it("explicit defaultProfile config overrides defaults in headless mode", () => {
const resolved = resolveBrowserConfig({
headless: true,
defaultProfile: "chrome",
defaultProfile: "chrome-relay",
});
expect(resolved.defaultProfile).toBe("chrome");
expect(resolved.defaultProfile).toBe("chrome-relay");
});
it("explicit defaultProfile config overrides defaults in noSandbox mode", () => {
const resolved = resolveBrowserConfig({
noSandbox: true,
defaultProfile: "chrome",
defaultProfile: "chrome-relay",
});
expect(resolved.defaultProfile).toBe("chrome");
expect(resolved.defaultProfile).toBe("chrome-relay");
});
it("allows custom profile as default even in headless mode", () => {

View File

@@ -180,17 +180,35 @@ function ensureDefaultProfile(
}
/**
* Ensure a built-in "chrome" profile exists for the Chrome extension relay.
* Ensure a built-in "user" profile exists for Chrome's existing-session attach flow.
*/
function ensureDefaultUserBrowserProfile(
profiles: Record<string, BrowserProfileConfig>,
): Record<string, BrowserProfileConfig> {
const result = { ...profiles };
if (result.user) {
return result;
}
result.user = {
driver: "existing-session",
attachOnly: true,
color: "#00AA00",
};
return result;
}
/**
* Ensure a built-in "chrome-relay" profile exists for the Chrome extension relay.
*
* Note: this is an OpenClaw browser profile (routing config), not a Chrome user profile.
* It points at the local relay CDP endpoint (controlPort + 1).
*/
function ensureDefaultChromeExtensionProfile(
function ensureDefaultChromeRelayProfile(
profiles: Record<string, BrowserProfileConfig>,
controlPort: number,
): Record<string, BrowserProfileConfig> {
const result = { ...profiles };
if (result.chrome) {
if (result["chrome-relay"]) {
return result;
}
const relayPort = controlPort + 1;
@@ -202,7 +220,7 @@ function ensureDefaultChromeExtensionProfile(
if (getUsedPorts(result).has(relayPort)) {
return result;
}
result.chrome = {
result["chrome-relay"] = {
driver: "extension",
cdpUrl: `http://127.0.0.1:${relayPort}`,
color: "#00AA00",
@@ -268,13 +286,15 @@ export function resolveBrowserConfig(
const legacyCdpPort = rawCdpUrl ? cdpInfo.port : undefined;
const isWsUrl = cdpInfo.parsed.protocol === "ws:" || cdpInfo.parsed.protocol === "wss:";
const legacyCdpUrl = rawCdpUrl && isWsUrl ? cdpInfo.normalized : undefined;
const profiles = ensureDefaultChromeExtensionProfile(
ensureDefaultProfile(
cfg?.profiles,
defaultColor,
legacyCdpPort,
cdpPortRangeStart,
legacyCdpUrl,
const profiles = ensureDefaultChromeRelayProfile(
ensureDefaultUserBrowserProfile(
ensureDefaultProfile(
cfg?.profiles,
defaultColor,
legacyCdpPort,
cdpPortRangeStart,
legacyCdpUrl,
),
),
controlPort,
);
@@ -286,7 +306,7 @@ export function resolveBrowserConfig(
? DEFAULT_BROWSER_DEFAULT_PROFILE_NAME
: profiles[DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME]
? DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME
: "chrome");
: "user");
const extraArgs = Array.isArray(cfg?.extraArgs)
? cfg.extraArgs.filter((a): a is string => typeof a === "string" && a.trim().length > 0)

View File

@@ -3,9 +3,9 @@ import { resolveBrowserConfig, resolveProfile } from "../config.js";
import { resolveSnapshotPlan } from "./agent.snapshot.plan.js";
describe("resolveSnapshotPlan", () => {
it("defaults chrome extension relay snapshots to aria when format is omitted", () => {
it("defaults chrome-relay snapshots to aria when format is omitted", () => {
const resolved = resolveBrowserConfig({});
const profile = resolveProfile(resolved, "chrome");
const profile = resolveProfile(resolved, "chrome-relay");
expect(profile).toBeTruthy();
const plan = resolveSnapshotPlan({

View File

@@ -25,9 +25,9 @@ function makeBrowserState(): BrowserServerState {
headless: true,
noSandbox: false,
attachOnly: false,
defaultProfile: "chrome",
defaultProfile: "chrome-relay",
profiles: {
chrome: {
"chrome-relay": {
driver: "extension",
cdpUrl: "http://127.0.0.1:18792",
cdpPort: 18792,

View File

@@ -43,7 +43,7 @@ describe("ensureExtensionRelayForProfiles", () => {
it("starts relay only for extension profiles", async () => {
resolveProfileMock.mockImplementation((_resolved: unknown, name: string) => {
if (name === "chrome") {
if (name === "chrome-relay") {
return { driver: "extension", cdpUrl: "http://127.0.0.1:18888" };
}
return { driver: "openclaw", cdpUrl: "http://127.0.0.1:18889" };
@@ -53,7 +53,7 @@ describe("ensureExtensionRelayForProfiles", () => {
await ensureExtensionRelayForProfiles({
resolved: {
profiles: {
chrome: {},
"chrome-relay": {},
openclaw: {},
},
} as never,
@@ -72,12 +72,12 @@ describe("ensureExtensionRelayForProfiles", () => {
const onWarn = vi.fn();
await ensureExtensionRelayForProfiles({
resolved: { profiles: { chrome: {} } } as never,
resolved: { profiles: { "chrome-relay": {} } } as never,
onWarn,
});
expect(onWarn).toHaveBeenCalledWith(
'Chrome extension relay init failed for profile "chrome": Error: boom',
'Chrome extension relay init failed for profile "chrome-relay": Error: boom',
);
});
});
@@ -91,10 +91,10 @@ describe("stopKnownBrowserProfiles", () => {
});
it("stops all known profiles and ignores per-profile failures", async () => {
listKnownProfileNamesMock.mockReturnValue(["openclaw", "chrome"]);
listKnownProfileNamesMock.mockReturnValue(["openclaw", "chrome-relay"]);
const stopMap: Record<string, ReturnType<typeof vi.fn>> = {
openclaw: vi.fn(async () => {}),
chrome: vi.fn(async () => {
"chrome-relay": vi.fn(async () => {
throw new Error("profile stop failed");
}),
};
@@ -112,7 +112,7 @@ describe("stopKnownBrowserProfiles", () => {
});
expect(stopMap.openclaw).toHaveBeenCalledTimes(1);
expect(stopMap.chrome).toHaveBeenCalledTimes(1);
expect(stopMap["chrome-relay"]).toHaveBeenCalledTimes(1);
expect(onWarn).not.toHaveBeenCalled();
});

View File

@@ -22,7 +22,7 @@ const configMocks = vi.hoisted(() => ({
const browserConfigMocks = vi.hoisted(() => ({
resolveBrowserConfig: vi.fn(() => ({
enabled: true,
defaultProfile: "chrome",
defaultProfile: "openclaw",
})),
}));
@@ -45,7 +45,7 @@ describe("runBrowserProxyCommand", () => {
});
browserConfigMocks.resolveBrowserConfig.mockReturnValue({
enabled: true,
defaultProfile: "chrome",
defaultProfile: "openclaw",
});
controlServiceMocks.startBrowserControlServiceFromConfig.mockResolvedValue(true);
});
@@ -70,12 +70,12 @@ describe("runBrowserProxyCommand", () => {
JSON.stringify({
method: "GET",
path: "/snapshot",
profile: "chrome",
profile: "chrome-relay",
timeoutMs: 5,
}),
),
).rejects.toThrow(
/browser proxy timed out for GET \/snapshot after 5ms; ws-backed browser action; profile=chrome; status\(running=true, cdpHttp=true, cdpReady=false, cdpUrl=http:\/\/127\.0\.0\.1:18792\)/,
/browser proxy timed out for GET \/snapshot after 5ms; ws-backed browser action; profile=chrome-relay; status\(running=true, cdpHttp=true, cdpReady=false, cdpUrl=http:\/\/127\.0\.0\.1:18792\)/,
);
});
@@ -100,12 +100,12 @@ describe("runBrowserProxyCommand", () => {
JSON.stringify({
method: "GET",
path: "/snapshot",
profile: "chrome-live",
profile: "user",
timeoutMs: 5,
}),
),
).rejects.toThrow(
/browser proxy timed out for GET \/snapshot after 5ms; ws-backed browser action; profile=chrome-live; status\(running=true, cdpHttp=true, cdpReady=false, transport=chrome-mcp\)/,
/browser proxy timed out for GET \/snapshot after 5ms; ws-backed browser action; profile=user; status\(running=true, cdpHttp=true, cdpReady=false, transport=chrome-mcp\)/,
);
});
@@ -120,7 +120,7 @@ describe("runBrowserProxyCommand", () => {
JSON.stringify({
method: "POST",
path: "/act",
profile: "chrome",
profile: "chrome-relay",
timeoutMs: 50,
}),
),