browser: drop chrome-relay auto-creation, simplify to user profile only

This commit is contained in:
George Zhang
2026-03-13 22:54:01 -07:00
parent 61d171ab0b
commit eca7a2ec32
8 changed files with 50 additions and 103 deletions

View File

@@ -157,11 +157,9 @@ describe("createOpenClawCodingTools", () => {
expect(schema.type).toBe("object");
expect(schema.anyOf).toBeUndefined();
});
it("mentions Chrome extension relay in browser tool description", () => {
it("mentions user browser profile in browser tool description", () => {
const browser = createBrowserTool();
expect(browser.description).toMatch(/Chrome extension/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");

View File

@@ -74,7 +74,7 @@ function formatConsoleToolResult(result: {
}
function isChromeStaleTargetError(profile: string | undefined, err: unknown): boolean {
if (profile !== "chrome-relay" && profile !== "chrome") {
if (profile !== "chrome-relay" && profile !== "chrome" && profile !== "user") {
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-relay" and use one of the returned targetIds.`,
`Chrome tab not found (stale targetId?). Run action=tabs profile="${profile}" and use one of the returned targetIds.`,
{ cause: err },
);
}

View File

@@ -287,9 +287,9 @@ describe("browser tool snapshot maxChars", () => {
expect(opts?.mode).toBeUndefined();
});
it("defaults to host when using profile=chrome-relay (even in sandboxed sessions)", async () => {
it("defaults to host when using an explicit extension relay profile (even in sandboxed sessions)", async () => {
setResolvedBrowserProfiles({
"chrome-relay": {
relay: {
driver: "extension",
cdpUrl: "http://127.0.0.1:18792",
color: "#0066CC",
@@ -298,14 +298,14 @@ describe("browser tool snapshot maxChars", () => {
const tool = createBrowserTool({ sandboxBridgeUrl: "http://127.0.0.1:9999" });
await tool.execute?.("call-1", {
action: "snapshot",
profile: "chrome-relay",
profile: "relay",
snapshotFormat: "ai",
});
expect(browserClientMocks.browserSnapshot).toHaveBeenCalledWith(
undefined,
expect.objectContaining({
profile: "chrome-relay",
profile: "relay",
}),
);
});
@@ -366,12 +366,12 @@ describe("browser tool snapshot maxChars", () => {
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-relay" });
await tool.execute?.("call-1", { action: "snapshot", profile: "user" });
expect(browserClientMocks.browserSnapshot).toHaveBeenCalledWith(
undefined,
expect.objectContaining({
profile: "chrome-relay",
profile: "user",
}),
);
const opts = browserClientMocks.browserSnapshot.mock.calls.at(-1)?.[1] as
@@ -438,21 +438,17 @@ describe("browser tool snapshot maxChars", () => {
expect(gatewayMocks.callGatewayTool).not.toHaveBeenCalled();
});
it("keeps chrome-relay profile on host when node proxy is available", async () => {
it("keeps user profile on host when node proxy is available", async () => {
mockSingleBrowserProxyNode();
setResolvedBrowserProfiles({
"chrome-relay": {
driver: "extension",
cdpUrl: "http://127.0.0.1:18792",
color: "#0066CC",
},
user: { driver: "existing-session", attachOnly: true, color: "#00AA00" },
});
const tool = createBrowserTool();
await tool.execute?.("call-1", { action: "status", profile: "chrome-relay" });
await tool.execute?.("call-1", { action: "status", profile: "user" });
expect(browserClientMocks.browserStatus).toHaveBeenCalledWith(
undefined,
expect.objectContaining({ profile: "chrome-relay" }),
expect.objectContaining({ profile: "user" }),
);
expect(gatewayMocks.callGatewayTool).not.toHaveBeenCalled();
});
@@ -745,7 +741,7 @@ describe("browser tool external content wrapping", () => {
describe("browser tool act stale target recovery", () => {
registerBrowserToolAfterEachReset();
it("retries safe chrome-relay act once without targetId when exactly one tab remains", async () => {
it("retries safe user-browser act once without targetId when exactly one tab remains", async () => {
browserActionsMocks.browserAct
.mockRejectedValueOnce(new Error("404: tab not found"))
.mockResolvedValueOnce({ ok: true });
@@ -754,7 +750,7 @@ describe("browser tool act stale target recovery", () => {
const tool = createBrowserTool();
const result = await tool.execute?.("call-1", {
action: "act",
profile: "chrome-relay",
profile: "user",
request: {
kind: "hover",
targetId: "stale-tab",
@@ -767,18 +763,18 @@ describe("browser tool act stale target recovery", () => {
1,
undefined,
expect.objectContaining({ targetId: "stale-tab", kind: "hover", ref: "btn-1" }),
expect.objectContaining({ profile: "chrome-relay" }),
expect.objectContaining({ profile: "user" }),
);
expect(browserActionsMocks.browserAct).toHaveBeenNthCalledWith(
2,
undefined,
expect.not.objectContaining({ targetId: expect.anything() }),
expect.objectContaining({ profile: "chrome-relay" }),
expect.objectContaining({ profile: "user" }),
);
expect(result?.details).toMatchObject({ ok: true });
});
it("does not retry mutating chrome-relay act requests without targetId", async () => {
it("does not retry mutating user-browser act requests without targetId", async () => {
browserActionsMocks.browserAct.mockRejectedValueOnce(new Error("404: tab not found"));
browserClientMocks.browserTabs.mockResolvedValueOnce([{ targetId: "only-tab" }]);
@@ -786,14 +782,14 @@ describe("browser tool act stale target recovery", () => {
await expect(
tool.execute?.("call-1", {
action: "act",
profile: "chrome-relay",
profile: "user",
request: {
kind: "click",
targetId: "stale-tab",
ref: "btn-1",
},
}),
).rejects.toThrow(/Run action=tabs profile="chrome-relay"/i);
).rejects.toThrow(/Run action=tabs profile="user"/i);
expect(browserActionsMocks.browserAct).toHaveBeenCalledTimes(1);
});

View File

@@ -294,7 +294,8 @@ function shouldPreferHostForProfile(profileName: string | undefined) {
}
function isHostOnlyProfileName(profileName: string | undefined) {
return profileName === "user" || profileName === "chrome-relay";
// User-browser profiles (existing-session, extension relay) are host-only.
return shouldPreferHostForProfile(profileName);
}
export function createBrowserTool(opts?: {
@@ -311,11 +312,8 @@ export function createBrowserTool(opts?: {
description: [
"Control the browser via OpenClaw's browser control server (status/start/stop/profiles/tabs/open/snapshot/screenshot/actions).",
"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.',
'For the logged-in user browser on the local host, use profile="user". Chrome (v146+) must be running. Use only when existing logins/cookies matter and the user is present.',
'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: 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.",

View File

@@ -193,7 +193,7 @@ async function createRealSession(profileName: string): Promise<ChromeMcpSession>
await client.close().catch(() => {});
throw new BrowserProfileUnavailableError(
`Chrome MCP existing-session attach failed for profile "${profileName}". ` +
`Make sure Chrome is running, enable chrome://inspect/#remote-debugging, and approve the connection. ` +
`Make sure Chrome (v146+) is running. ` +
`Details: ${String(err)}`,
);
}

View File

@@ -26,10 +26,8 @@ describe("browser config", () => {
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");
// chrome-relay is no longer auto-created
expect(resolveProfile(resolved, "chrome-relay")).toBe(null);
expect(resolved.remoteCdpTimeoutMs).toBe(1500);
expect(resolved.remoteCdpHandshakeTimeoutMs).toBe(3000);
});
@@ -38,10 +36,7 @@ describe("browser config", () => {
withEnv({ OPENCLAW_GATEWAY_PORT: "19001" }, () => {
const resolved = resolveBrowserConfig(undefined);
expect(resolved.controlPort).toBe(19003);
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");
expect(resolveProfile(resolved, "chrome-relay")).toBe(null);
const openclaw = resolveProfile(resolved, "openclaw");
expect(openclaw?.cdpPort).toBe(19012);
@@ -53,10 +48,7 @@ describe("browser config", () => {
withEnv({ OPENCLAW_GATEWAY_PORT: undefined }, () => {
const resolved = resolveBrowserConfig(undefined, { gateway: { port: 19011 } });
expect(resolved.controlPort).toBe(19013);
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");
expect(resolveProfile(resolved, "chrome-relay")).toBe(null);
const openclaw = resolveProfile(resolved, "openclaw");
expect(openclaw?.cdpPort).toBe(19022);
@@ -209,16 +201,6 @@ describe("browser config", () => {
);
});
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-relay")).toBe(null);
expect(resolved.defaultProfile).toBe("openclaw");
});
it("defaults extraArgs to empty array when not provided", () => {
const resolved = resolveBrowserConfig(undefined);
expect(resolved.extraArgs).toEqual([]);
@@ -307,6 +289,7 @@ describe("browser config", () => {
const resolved = resolveBrowserConfig({
profiles: {
"chrome-live": { driver: "existing-session", attachOnly: true, color: "#00AA00" },
relay: { driver: "extension", cdpUrl: "http://127.0.0.1:18792", color: "#0066CC" },
work: { cdpPort: 18801, color: "#0066CC" },
},
});
@@ -317,7 +300,7 @@ describe("browser config", () => {
const managed = resolveProfile(resolved, "openclaw")!;
expect(getBrowserProfileCapabilities(managed).usesChromeMcp).toBe(false);
const extension = resolveProfile(resolved, "chrome-relay")!;
const extension = resolveProfile(resolved, "relay")!;
expect(getBrowserProfileCapabilities(extension).usesChromeMcp).toBe(false);
const work = resolveProfile(resolved, "work")!;
@@ -358,17 +341,17 @@ describe("browser config", () => {
it("explicit defaultProfile config overrides defaults in headless mode", () => {
const resolved = resolveBrowserConfig({
headless: true,
defaultProfile: "chrome-relay",
defaultProfile: "user",
});
expect(resolved.defaultProfile).toBe("chrome-relay");
expect(resolved.defaultProfile).toBe("user");
});
it("explicit defaultProfile config overrides defaults in noSandbox mode", () => {
const resolved = resolveBrowserConfig({
noSandbox: true,
defaultProfile: "chrome-relay",
defaultProfile: "user",
});
expect(resolved.defaultProfile).toBe("chrome-relay");
expect(resolved.defaultProfile).toBe("user");
});
it("allows custom profile as default even in headless mode", () => {

View File

@@ -14,7 +14,7 @@ import {
DEFAULT_BROWSER_DEFAULT_PROFILE_NAME,
DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME,
} from "./constants.js";
import { CDP_PORT_RANGE_START, getUsedPorts } from "./profiles.js";
import { CDP_PORT_RANGE_START } from "./profiles.js";
export type ResolvedBrowserConfig = {
enabled: boolean;
@@ -197,36 +197,6 @@ function ensureDefaultUserBrowserProfile(
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 ensureDefaultChromeRelayProfile(
profiles: Record<string, BrowserProfileConfig>,
controlPort: number,
): Record<string, BrowserProfileConfig> {
const result = { ...profiles };
if (result["chrome-relay"]) {
return result;
}
const relayPort = controlPort + 1;
if (!Number.isFinite(relayPort) || relayPort <= 0 || relayPort > 65535) {
return result;
}
// Avoid adding the built-in profile if the derived relay port is already used by another profile
// (legacy single-profile configs may use controlPort+1 for openclaw/openclaw CDP).
if (getUsedPorts(result).has(relayPort)) {
return result;
}
result["chrome-relay"] = {
driver: "extension",
cdpUrl: `http://127.0.0.1:${relayPort}`,
color: "#00AA00",
};
return result;
}
export function resolveBrowserConfig(
cfg: BrowserConfig | undefined,
rootConfig?: OpenClawConfig,
@@ -286,17 +256,14 @@ 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 = ensureDefaultChromeRelayProfile(
ensureDefaultUserBrowserProfile(
ensureDefaultProfile(
cfg?.profiles,
defaultColor,
legacyCdpPort,
cdpPortRangeStart,
legacyCdpUrl,
),
const profiles = ensureDefaultUserBrowserProfile(
ensureDefaultProfile(
cfg?.profiles,
defaultColor,
legacyCdpPort,
cdpPortRangeStart,
legacyCdpUrl,
),
controlPort,
);
const cdpProtocol = cdpInfo.parsed.protocol === "https:" ? "https" : "http";

View File

@@ -3,10 +3,15 @@ import { resolveBrowserConfig, resolveProfile } from "../config.js";
import { resolveSnapshotPlan } from "./agent.snapshot.plan.js";
describe("resolveSnapshotPlan", () => {
it("defaults chrome-relay snapshots to aria when format is omitted", () => {
const resolved = resolveBrowserConfig({});
const profile = resolveProfile(resolved, "chrome-relay");
it("defaults extension relay snapshots to aria when format is omitted", () => {
const resolved = resolveBrowserConfig({
profiles: {
relay: { driver: "extension", cdpUrl: "http://127.0.0.1:18792", color: "#0066CC" },
},
});
const profile = resolveProfile(resolved, "relay");
expect(profile).toBeTruthy();
expect(profile?.driver).toBe("extension");
const plan = resolveSnapshotPlan({
profile: profile as NonNullable<typeof profile>,