mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-26 01:11:37 +00:00
browser: drop chrome-relay auto-creation, simplify to user profile only
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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)}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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>,
|
||||
|
||||
Reference in New Issue
Block a user