mirror of https://github.com/openclaw/openclaw.git
browser: default user-browser profile to existing-session (Chrome MCP)
This commit is contained in:
parent
f1d9fcd407
commit
f9aebafa7a
|
|
@ -371,11 +371,7 @@ openclaw browser create-profile \
|
|||
--color "#00AA00"
|
||||
```
|
||||
|
||||
Then in Chrome:
|
||||
|
||||
1. Open `chrome://inspect/#remote-debugging`
|
||||
2. Enable remote debugging
|
||||
3. Keep Chrome running and approve the connection prompt when OpenClaw attaches
|
||||
Then keep Chrome (v146+) running. OpenClaw auto-discovers it — no setup needed.
|
||||
|
||||
Live attach smoke test:
|
||||
|
||||
|
|
@ -396,17 +392,15 @@ What success looks like:
|
|||
|
||||
What to check if attach does not work:
|
||||
|
||||
- Chrome is version `144+`
|
||||
- remote debugging is enabled at `chrome://inspect/#remote-debugging`
|
||||
- Chrome showed and you accepted the attach consent prompt
|
||||
- Chrome is version `146+` and running
|
||||
- no other tool is already attached to the same Chrome session
|
||||
|
||||
Agent use:
|
||||
|
||||
- Use `browserSession="user"` when you need the user’s logged-in browser state.
|
||||
- If you know the profile name, pass `profile="chrome-live"` (or your custom
|
||||
existing-session profile).
|
||||
- Only choose this mode when the user is at the computer to approve the attach
|
||||
prompt.
|
||||
- Only choose this mode when the user is at the computer.
|
||||
- the Gateway or node host can spawn `npx chrome-devtools-mcp@latest --autoConnect`
|
||||
|
||||
Notes:
|
||||
|
|
|
|||
|
|
@ -311,10 +311,10 @@ describe("browser tool snapshot maxChars", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('uses the host user browser for browserSession="user"', async () => {
|
||||
it('resolves sole user profile for browserSession="user"', async () => {
|
||||
setResolvedBrowserProfiles({
|
||||
openclaw: { cdpPort: 18800, color: "#FF4500" },
|
||||
chrome: { driver: "extension", cdpUrl: "http://127.0.0.1:18792", color: "#0066CC" },
|
||||
chrome: { driver: "existing-session", attachOnly: true, color: "#00AA00" },
|
||||
});
|
||||
const tool = createBrowserTool({ sandboxBridgeUrl: "http://127.0.0.1:9999" });
|
||||
await tool.execute?.("call-1", {
|
||||
|
|
@ -331,31 +331,11 @@ describe("browser tool snapshot maxChars", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('uses a sole existing-session profile for browserSession="user"', async () => {
|
||||
it('lists available profiles when browserSession="user" is ambiguous', 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",
|
||||
snapshotFormat: "ai",
|
||||
});
|
||||
|
||||
expect(browserClientMocks.browserSnapshot).toHaveBeenCalledWith(
|
||||
undefined,
|
||||
expect.objectContaining({
|
||||
profile: "chrome-live",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('fails when browserSession="user" is ambiguous', async () => {
|
||||
setResolvedBrowserProfiles({
|
||||
openclaw: { cdpPort: 18800, color: "#FF4500" },
|
||||
personal: { driver: "existing-session", attachOnly: true, color: "#00AA00" },
|
||||
work: { driver: "existing-session", attachOnly: true, color: "#0066CC" },
|
||||
chrome: { driver: "existing-session", attachOnly: true, color: "#00AA00" },
|
||||
relay: { driver: "extension", cdpUrl: "http://127.0.0.1:18792", color: "#0066CC" },
|
||||
});
|
||||
const tool = createBrowserTool();
|
||||
|
||||
|
|
@ -365,12 +345,12 @@ describe("browser tool snapshot maxChars", () => {
|
|||
browserSession: "user",
|
||||
snapshotFormat: "ai",
|
||||
}),
|
||||
).rejects.toThrow(/Multiple user-browser profiles are configured/);
|
||||
).rejects.toThrow(/Multiple user-browser profiles available.*profile=".*"/s);
|
||||
});
|
||||
|
||||
it('rejects browserSession="user" with target="sandbox"', async () => {
|
||||
setResolvedBrowserProfiles({
|
||||
chrome: { driver: "extension", cdpUrl: "http://127.0.0.1:18792", color: "#0066CC" },
|
||||
chrome: { driver: "existing-session", attachOnly: true, color: "#00AA00" },
|
||||
});
|
||||
const tool = createBrowserTool({ sandboxBridgeUrl: "http://127.0.0.1:9999" });
|
||||
|
||||
|
|
|
|||
|
|
@ -280,18 +280,13 @@ function resolveBrowserBaseUrl(params: {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
function listUserBrowserProfiles() {
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve which browser profile to use for a given browserSession value.
|
||||
*
|
||||
* For browserSession="user": if exactly one user-capable profile exists, use
|
||||
* it. Otherwise return undefined and let the caller list available profiles so
|
||||
* the model (or user) can pick.
|
||||
*/
|
||||
function resolveBrowserToolProfile(params: {
|
||||
profile?: string;
|
||||
browserSession?: "agent" | "user";
|
||||
|
|
@ -306,31 +301,24 @@ function resolveBrowserToolProfile(params: {
|
|||
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;
|
||||
}
|
||||
// Find all profiles that connect to the user's real browser.
|
||||
const cfg = loadConfig();
|
||||
const resolved = resolveBrowserConfig(cfg.browser, cfg);
|
||||
const userProfiles = Object.keys(resolved.profiles ?? {})
|
||||
.map((name) => resolveProfile(resolved, name))
|
||||
.filter((profile): profile is NonNullable<typeof profile> => Boolean(profile))
|
||||
.filter((profile) => getBrowserProfileCapabilities(profile).isUserBrowser);
|
||||
|
||||
if (userProfiles.length === 1) {
|
||||
return userProfiles[0]?.name;
|
||||
}
|
||||
const chromeLive = userProfiles.find((profile) => profile.name === "chrome-live");
|
||||
if (chromeLive) {
|
||||
return chromeLive.name;
|
||||
return userProfiles[0].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("No user-browser profile found. Set up an existing-session profile first.");
|
||||
}
|
||||
// Multiple — tell the model what's available so it can pick or ask the user.
|
||||
const descriptions = userProfiles.map((p) => ` - profile="${p.name}" (${p.driver})`).join("\n");
|
||||
throw new Error(
|
||||
`Multiple user-browser profiles are configured (${userProfiles.map((profile) => profile.name).join(", ")}). Pass profile="<name>".`,
|
||||
`Multiple user-browser profiles available. Pick one with profile="<name>" or ask the user:\n${descriptions}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -347,12 +335,11 @@ 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).',
|
||||
'browserSession="agent" (default): isolated OpenClaw-managed browser — fresh profile, no cookies/logins.',
|
||||
'browserSession="user": the user\'s real local browser with their logged-in sessions. Use when you need their cookies/auth. Chrome (v146+) must be running. If unsure whether the user is present, ask first.',
|
||||
'If browserSession="user" can\'t auto-resolve (multiple profiles), the error lists available profiles — pick one with profile="<name>" or ask the user.',
|
||||
'profile is an explicit override and always takes precedence over browserSession. Use action="profiles" to discover available profiles.',
|
||||
'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.",
|
||||
"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)}`,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,9 +23,10 @@ describe("browser config", () => {
|
|||
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");
|
||||
expect(chrome?.driver).toBe("existing-session");
|
||||
expect(chrome?.cdpPort).toBe(0);
|
||||
expect(chrome?.cdpUrl).toBe("");
|
||||
expect(chrome?.attachOnly).toBe(true);
|
||||
expect(resolved.remoteCdpTimeoutMs).toBe(1500);
|
||||
expect(resolved.remoteCdpHandshakeTimeoutMs).toBe(3000);
|
||||
});
|
||||
|
|
@ -35,9 +36,7 @@ describe("browser config", () => {
|
|||
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");
|
||||
expect(chrome?.driver).toBe("existing-session");
|
||||
|
||||
const openclaw = resolveProfile(resolved, "openclaw");
|
||||
expect(openclaw?.cdpPort).toBe(19012);
|
||||
|
|
@ -50,9 +49,7 @@ describe("browser config", () => {
|
|||
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");
|
||||
expect(chrome?.driver).toBe("existing-session");
|
||||
|
||||
const openclaw = resolveProfile(resolved, "openclaw");
|
||||
expect(openclaw?.cdpPort).toBe(19022);
|
||||
|
|
@ -205,14 +202,16 @@ 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 profile if the user already has one", () => {
|
||||
const resolved = resolveBrowserConfig({
|
||||
profiles: {
|
||||
openclaw: { cdpPort: 18792, color: "#FF4500" },
|
||||
chrome: { driver: "extension", cdpUrl: "http://127.0.0.1:18792", color: "#0066CC" },
|
||||
},
|
||||
});
|
||||
expect(resolveProfile(resolved, "chrome")).toBe(null);
|
||||
expect(resolved.defaultProfile).toBe("openclaw");
|
||||
// User's explicit chrome profile is preserved, not overwritten
|
||||
const chrome = resolveProfile(resolved, "chrome");
|
||||
expect(chrome?.driver).toBe("extension");
|
||||
expect(chrome?.cdpUrl).toBe("http://127.0.0.1:18792");
|
||||
});
|
||||
|
||||
it("defaults extraArgs to empty array when not provided", () => {
|
||||
|
|
@ -303,6 +302,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" },
|
||||
},
|
||||
});
|
||||
|
|
@ -313,7 +313,7 @@ describe("browser config", () => {
|
|||
const managed = resolveProfile(resolved, "openclaw")!;
|
||||
expect(getBrowserProfileCapabilities(managed).usesChromeMcp).toBe(false);
|
||||
|
||||
const extension = resolveProfile(resolved, "chrome")!;
|
||||
const extension = resolveProfile(resolved, "relay")!;
|
||||
expect(getBrowserProfileCapabilities(extension).usesChromeMcp).toBe(false);
|
||||
|
||||
const work = resolveProfile(resolved, "work")!;
|
||||
|
|
|
|||
|
|
@ -8,13 +8,14 @@ import {
|
|||
import { isLoopbackHost } from "../gateway/net.js";
|
||||
import type { SsrFPolicy } from "../infra/net/ssrf.js";
|
||||
import {
|
||||
MANAGED_BROWSER_DRIVERS,
|
||||
DEFAULT_OPENCLAW_BROWSER_COLOR,
|
||||
DEFAULT_OPENCLAW_BROWSER_ENABLED,
|
||||
DEFAULT_BROWSER_EVALUATE_ENABLED,
|
||||
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;
|
||||
|
|
@ -180,31 +181,31 @@ function ensureDefaultProfile(
|
|||
}
|
||||
|
||||
/**
|
||||
* Ensure a built-in "chrome" profile exists for the Chrome extension relay.
|
||||
* Ensure a built-in "chrome" profile exists for connecting to the user's
|
||||
* real Chrome browser.
|
||||
*
|
||||
* Note: this is an OpenClaw browser profile (routing config), not a Chrome user profile.
|
||||
* It points at the local relay CDP endpoint (controlPort + 1).
|
||||
* Uses the existing-session driver (Chrome MCP) which connects via
|
||||
* Chrome MCP auto-discovers a running Chrome (v146+) via its user data
|
||||
* directory — no CDP port or remote-debugging toggle needed.
|
||||
*/
|
||||
function ensureDefaultChromeExtensionProfile(
|
||||
function ensureDefaultUserBrowserProfile(
|
||||
profiles: Record<string, BrowserProfileConfig>,
|
||||
controlPort: number,
|
||||
): Record<string, BrowserProfileConfig> {
|
||||
const result = { ...profiles };
|
||||
if (result.chrome) {
|
||||
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)) {
|
||||
// Skip when the user already has a profile that connects to their real
|
||||
// browser (any driver not in MANAGED_BROWSER_DRIVERS).
|
||||
const hasUserProfile = Object.values(result).some(
|
||||
(p) => p.driver && !MANAGED_BROWSER_DRIVERS.has(p.driver),
|
||||
);
|
||||
if (hasUserProfile) {
|
||||
return result;
|
||||
}
|
||||
result.chrome = {
|
||||
driver: "extension",
|
||||
cdpUrl: `http://127.0.0.1:${relayPort}`,
|
||||
driver: "existing-session",
|
||||
attachOnly: true,
|
||||
color: "#00AA00",
|
||||
};
|
||||
return result;
|
||||
|
|
@ -268,7 +269,7 @@ 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(
|
||||
const profiles = ensureDefaultUserBrowserProfile(
|
||||
ensureDefaultProfile(
|
||||
cfg?.profiles,
|
||||
defaultColor,
|
||||
|
|
@ -276,7 +277,6 @@ export function resolveBrowserConfig(
|
|||
cdpPortRangeStart,
|
||||
legacyCdpUrl,
|
||||
),
|
||||
controlPort,
|
||||
);
|
||||
const cdpProtocol = cdpInfo.parsed.protocol === "https:" ? "https" : "http";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
/**
|
||||
* Drivers that launch/manage a browser instance (sandbox). Any driver not in
|
||||
* this set attaches to the user's existing browser.
|
||||
*/
|
||||
export const MANAGED_BROWSER_DRIVERS = new Set(["openclaw", "clawd"]);
|
||||
|
||||
export const DEFAULT_OPENCLAW_BROWSER_ENABLED = true;
|
||||
export const DEFAULT_BROWSER_EVALUATE_ENABLED = true;
|
||||
export const DEFAULT_OPENCLAW_BROWSER_COLOR = "#FF4500";
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ export type BrowserProfileMode =
|
|||
export type BrowserProfileCapabilities = {
|
||||
mode: BrowserProfileMode;
|
||||
isRemote: boolean;
|
||||
/** Profile attaches to the user's real browser (not a managed sandbox). */
|
||||
isUserBrowser: boolean;
|
||||
/** Profile uses the Chrome DevTools MCP server (existing-session driver). */
|
||||
usesChromeMcp: boolean;
|
||||
requiresRelay: boolean;
|
||||
|
|
@ -27,6 +29,7 @@ export function getBrowserProfileCapabilities(
|
|||
return {
|
||||
mode: "local-extension-relay",
|
||||
isRemote: false,
|
||||
isUserBrowser: true,
|
||||
usesChromeMcp: false,
|
||||
requiresRelay: true,
|
||||
requiresAttachedTab: true,
|
||||
|
|
@ -42,6 +45,7 @@ export function getBrowserProfileCapabilities(
|
|||
return {
|
||||
mode: "local-existing-session",
|
||||
isRemote: false,
|
||||
isUserBrowser: true,
|
||||
usesChromeMcp: true,
|
||||
requiresRelay: false,
|
||||
requiresAttachedTab: false,
|
||||
|
|
@ -57,6 +61,7 @@ export function getBrowserProfileCapabilities(
|
|||
return {
|
||||
mode: "remote-cdp",
|
||||
isRemote: true,
|
||||
isUserBrowser: false,
|
||||
usesChromeMcp: false,
|
||||
requiresRelay: false,
|
||||
requiresAttachedTab: false,
|
||||
|
|
@ -71,6 +76,7 @@ export function getBrowserProfileCapabilities(
|
|||
return {
|
||||
mode: "local-managed",
|
||||
isRemote: false,
|
||||
isUserBrowser: false,
|
||||
usesChromeMcp: false,
|
||||
requiresRelay: false,
|
||||
requiresAttachedTab: false,
|
||||
|
|
|
|||
|
|
@ -3,10 +3,11 @@ 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 existing-session snapshots to ai when format is omitted", () => {
|
||||
const resolved = resolveBrowserConfig({});
|
||||
const profile = resolveProfile(resolved, "chrome");
|
||||
expect(profile).toBeTruthy();
|
||||
expect(profile?.driver).toBe("existing-session");
|
||||
|
||||
const plan = resolveSnapshotPlan({
|
||||
profile: profile as NonNullable<typeof profile>,
|
||||
|
|
@ -14,7 +15,7 @@ describe("resolveSnapshotPlan", () => {
|
|||
hasPlaywright: true,
|
||||
});
|
||||
|
||||
expect(plan.format).toBe("aria");
|
||||
expect(plan.format).toBe("ai");
|
||||
});
|
||||
|
||||
it("keeps ai snapshots for managed browsers when Playwright is available", () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue