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
No known key found for this signature in database
21 changed files with 211 additions and 247 deletions

View File

@ -11,7 +11,7 @@ Docs: https://docs.openclaw.ai
- Android/chat settings: redesign the chat settings sheet with grouped device and media sections, refresh the Connect and Voice tabs, and tighten the chat composer/session header for a denser mobile layout. (#44894) Thanks @obviyus. - Android/chat settings: redesign the chat settings sheet with grouped device and media sections, refresh the Connect and Voice tabs, and tighten the chat composer/session header for a denser mobile layout. (#44894) Thanks @obviyus.
- iOS/onboarding: add a first-run welcome pager before gateway setup, stop auto-opening the QR scanner, and show `/pair qr` instructions on the connect step. (#45054) Thanks @ngutman. - iOS/onboarding: add a first-run welcome pager before gateway setup, stop auto-opening the QR scanner, and show `/pair qr` instructions on the connect step. (#45054) Thanks @ngutman.
- Docker/timezone override: add `OPENCLAW_TZ` so `docker-setup.sh` can pin gateway and CLI containers to a chosen IANA timezone instead of inheriting the daemon default. (#34119) Thanks @Lanfei. - Docker/timezone override: add `OPENCLAW_TZ` so `docker-setup.sh` can pin gateway and CLI containers to a chosen IANA timezone instead of inheriting the daemon default. (#34119) Thanks @Lanfei.
- Browser/agents: add `browserSession="agent" | "user"` so agent browser calls can explicitly choose the isolated OpenClaw browser or a logged-in user browser, with docs for when user presence and attach approval are required. - Browser/agents: add built-in `profile="user"` for the logged-in host browser and `profile="chrome-relay"` for the extension relay, so agent browser calls can prefer the real signed-in browser without the extra `browserSession` selector.
### Fixes ### Fixes

View File

@ -27,7 +27,7 @@ Related:
## Quick start (local) ## Quick start (local)
```bash ```bash
openclaw browser --browser-profile chrome tabs openclaw browser profiles
openclaw browser --browser-profile openclaw start openclaw browser --browser-profile openclaw start
openclaw browser --browser-profile openclaw open https://example.com openclaw browser --browser-profile openclaw open https://example.com
openclaw browser --browser-profile openclaw snapshot openclaw browser --browser-profile openclaw snapshot
@ -38,7 +38,8 @@ openclaw browser --browser-profile openclaw snapshot
Profiles are named browser routing configs. In practice: Profiles are named browser routing configs. In practice:
- `openclaw`: launches/attaches to a dedicated OpenClaw-managed Chrome instance (isolated user data dir). - `openclaw`: launches/attaches to a dedicated OpenClaw-managed Chrome instance (isolated user data dir).
- `chrome`: controls your existing Chrome tab(s) via the Chrome extension relay. - `user`: controls your existing signed-in Chrome session via Chrome DevTools MCP.
- `chrome-relay`: controls your existing Chrome tab(s) via the Chrome extension relay.
```bash ```bash
openclaw browser profiles openclaw browser profiles

View File

@ -2342,7 +2342,7 @@ See [Plugins](/tools/plugin).
browser: { browser: {
enabled: true, enabled: true,
evaluateEnabled: true, evaluateEnabled: true,
defaultProfile: "chrome", defaultProfile: "user",
ssrfPolicy: { ssrfPolicy: {
dangerouslyAllowPrivateNetwork: true, // default trusted-network mode dangerouslyAllowPrivateNetwork: true, // default trusted-network mode
// allowPrivateNetwork: true, // legacy alias // allowPrivateNetwork: true, // legacy alias

View File

@ -289,7 +289,7 @@ Look for:
- Valid browser executable path. - Valid browser executable path.
- CDP profile reachability. - CDP profile reachability.
- Extension relay tab attachment for `profile="chrome"`. - Extension relay tab attachment for `profile="chrome-relay"`.
Common signatures: Common signatures:

View File

@ -123,7 +123,7 @@ curl -s http://127.0.0.1:18791/tabs
### Problem: "Chrome extension relay is running, but no tab is connected" ### Problem: "Chrome extension relay is running, but no tab is connected"
Youre using the `chrome` profile (extension relay). It expects the OpenClaw Youre using the `chrome-relay` profile (extension relay). It expects the OpenClaw
browser extension to be attached to a live tab. browser extension to be attached to a live tab.
Fix options: Fix options:
@ -135,5 +135,5 @@ Fix options:
Notes: Notes:
- The `chrome` profile uses your **system default Chromium browser** when possible. - The `chrome-relay` profile uses your **system default Chromium browser** when possible.
- Local `openclaw` profiles auto-assign `cdpPort`/`cdpUrl`; only set those for remote CDP. - Local `openclaw` profiles auto-assign `cdpPort`/`cdpUrl`; only set those for remote CDP.

View File

@ -23,8 +23,8 @@ OpenClaw controls a **dedicated Chrome profile** (named `openclaw`, orangetin
For agent browser tool calls: For agent browser tool calls:
- Default choice: the agent should use its isolated `openclaw` browser. - Default choice: the agent should use its isolated `openclaw` browser.
- Use the **user browser** only when existing logged-in sessions matter and the user is at the computer to click/approve any attach prompt. - Use `profile="user"` only when existing logged-in sessions matter and the user is at the computer to click/approve any attach prompt.
- If you need to force the choice, use `browserSession="agent"` or `browserSession="user"`. - Use `profile="chrome-relay"` only for the Chrome extension / toolbar-button attach flow.
- If you have multiple user-browser profiles, specify the profile explicitly instead of guessing. - If you have multiple user-browser profiles, specify the profile explicitly instead of guessing.
Two easy ways to access it: Two easy ways to access it:

View File

@ -33,7 +33,7 @@ Choose this when:
### Option 2: Chrome extension relay ### Option 2: Chrome extension relay
Use the built-in `chrome` profile plus the OpenClaw Chrome extension. Use the built-in `chrome-relay` profile plus the OpenClaw Chrome extension.
Choose this when: Choose this when:
@ -155,7 +155,7 @@ Example:
{ {
browser: { browser: {
enabled: true, enabled: true,
defaultProfile: "chrome", defaultProfile: "chrome-relay",
relayBindHost: "0.0.0.0", relayBindHost: "0.0.0.0",
}, },
} }
@ -197,7 +197,7 @@ openclaw browser tabs --browser-profile remote
For the extension relay: For the extension relay:
```bash ```bash
openclaw browser tabs --browser-profile chrome openclaw browser tabs --browser-profile chrome-relay
``` ```
Good result: Good result:

View File

@ -18,8 +18,8 @@ Beginner view:
- Think of it as a **separate, agent-only browser**. - Think of it as a **separate, agent-only browser**.
- The `openclaw` profile does **not** touch your personal browser profile. - The `openclaw` profile does **not** touch your personal browser profile.
- The agent can **open tabs, read pages, click, and type** in a safe lane. - The agent can **open tabs, read pages, click, and type** in a safe lane.
- The default `chrome` profile uses the **system default Chromium browser** via the - The built-in `user` profile attaches to your real signed-in Chrome session;
extension relay; switch to `openclaw` for the isolated managed browser. `chrome-relay` is the explicit extension-relay profile.
## What you get ## What you get
@ -43,22 +43,22 @@ openclaw browser --browser-profile openclaw snapshot
If you get “Browser disabled”, enable it in config (see below) and restart the If you get “Browser disabled”, enable it in config (see below) and restart the
Gateway. Gateway.
## Profiles: `openclaw` vs `chrome` ## Profiles: `openclaw` vs `user` vs `chrome-relay`
- `openclaw`: managed, isolated browser (no extension required). - `openclaw`: managed, isolated browser (no extension required).
- `chrome`: extension relay to your **system browser** (requires the OpenClaw - `user`: built-in Chrome MCP attach profile for your **real signed-in Chrome**
extension to be attached to a tab). session.
- `existing-session`: official Chrome MCP attach flow for a running Chrome - `chrome-relay`: extension relay to your **system browser** (requires the
profile. OpenClaw extension to be attached to a tab).
For agent browser tool calls: For agent browser tool calls:
- Default: use the isolated `openclaw` browser. - Default: use the isolated `openclaw` browser.
- Use the **user browser** only when existing logged-in sessions matter and the - Prefer `profile="user"` when existing logged-in sessions matter and the user
user is at the computer to click/approve any attach prompt. is at the computer to click/approve any attach prompt.
- If you need to force the choice, use `browserSession="agent"` or - Use `profile="chrome-relay"` only when the user explicitly wants the Chrome
`browserSession="user"`. extension / toolbar-button attach flow.
- `profile` is the explicit override when you already know which profile to use. - `profile` is the explicit override when you want a specific browser mode.
Set `browser.defaultProfile: "openclaw"` if you want managed mode by default. Set `browser.defaultProfile: "openclaw"` if you want managed mode by default.
@ -88,11 +88,16 @@ Browser settings live in `~/.openclaw/openclaw.json`.
profiles: { profiles: {
openclaw: { cdpPort: 18800, color: "#FF4500" }, openclaw: { cdpPort: 18800, color: "#FF4500" },
work: { cdpPort: 18801, color: "#0066CC" }, work: { cdpPort: 18801, color: "#0066CC" },
"chrome-live": { user: {
driver: "existing-session", driver: "existing-session",
attachOnly: true, attachOnly: true,
color: "#00AA00", color: "#00AA00",
}, },
"chrome-relay": {
driver: "extension",
cdpUrl: "http://127.0.0.1:18792",
color: "#00AA00",
},
remote: { cdpUrl: "http://10.0.0.42:9222", color: "#00AA00" }, remote: { cdpUrl: "http://10.0.0.42:9222", color: "#00AA00" },
}, },
}, },
@ -113,7 +118,7 @@ Notes:
- `browser.ssrfPolicy.allowPrivateNetwork` remains supported as a legacy alias for compatibility. - `browser.ssrfPolicy.allowPrivateNetwork` remains supported as a legacy alias for compatibility.
- `attachOnly: true` means “never launch a local browser; only attach if it is already running.” - `attachOnly: true` means “never launch a local browser; only attach if it is already running.”
- `color` + per-profile `color` tint the browser UI so you can see which profile is active. - `color` + per-profile `color` tint the browser UI so you can see which profile is active.
- Default profile is `openclaw` (OpenClaw-managed standalone browser). Use `defaultProfile: "chrome"` to opt into the Chrome extension relay. - Default profile is `openclaw` (OpenClaw-managed standalone browser). Use `defaultProfile: "user"` to opt into the signed-in user browser, or `defaultProfile: "chrome-relay"` for the extension relay.
- Auto-detect order: system default browser if Chromium-based; otherwise Chrome → Brave → Edge → Chromium → Chrome Canary. - Auto-detect order: system default browser if Chromium-based; otherwise Chrome → Brave → Edge → Chromium → Chrome Canary.
- Local `openclaw` profiles auto-assign `cdpPort`/`cdpUrl` — set those only for remote CDP. - Local `openclaw` profiles auto-assign `cdpPort`/`cdpUrl` — set those only for remote CDP.
- `driver: "existing-session"` uses Chrome DevTools MCP instead of raw CDP. Do - `driver: "existing-session"` uses Chrome DevTools MCP instead of raw CDP. Do
@ -287,7 +292,7 @@ OpenClaw supports multiple named profiles (routing configs). Profiles can be:
Defaults: Defaults:
- The `openclaw` profile is auto-created if missing. - The `openclaw` profile is auto-created if missing.
- The `chrome` profile is built-in for the Chrome extension relay (points at `http://127.0.0.1:18792` by default). - The `chrome-relay` profile is built-in for the Chrome extension relay (points at `http://127.0.0.1:18792` by default).
- Existing-session profiles are opt-in; create them with `--driver existing-session`. - Existing-session profiles are opt-in; create them with `--driver existing-session`.
- Local CDP ports allocate from **1880018899** by default. - Local CDP ports allocate from **1880018899** by default.
- Deleting a profile moves its local data directory to Trash. - Deleting a profile moves its local data directory to Trash.
@ -331,8 +336,8 @@ openclaw browser extension install
2. Use it: 2. Use it:
- CLI: `openclaw browser --browser-profile chrome tabs` - CLI: `openclaw browser --browser-profile chrome-relay tabs`
- Agent tool: `browser` with `browserSession="user"` (or `profile="chrome"`) - Agent tool: `browser` with `profile="chrome-relay"`
Optional: if you want a different name or relay port, create your own profile: Optional: if you want a different name or relay port, create your own profile:
@ -348,8 +353,9 @@ Notes:
- This mode relies on Playwright-on-CDP for most operations (screenshots/snapshots/actions). - This mode relies on Playwright-on-CDP for most operations (screenshots/snapshots/actions).
- Detach by clicking the extension icon again. - Detach by clicking the extension icon again.
- Agent use: prefer `browserSession="user"` for logged-in sites. The user must be - Agent use: prefer `profile="user"` for logged-in sites. Use `profile="chrome-relay"`
present to click the extension and attach the tab. only when you specifically want the extension flow. The user must be present
to click the extension and attach the tab.
## Chrome existing-session via MCP ## Chrome existing-session via MCP
@ -362,14 +368,12 @@ Official background and setup references:
- [Chrome for Developers: Use Chrome DevTools MCP with your browser session](https://developer.chrome.com/blog/chrome-devtools-mcp-debug-your-browser-session) - [Chrome for Developers: Use Chrome DevTools MCP with your browser session](https://developer.chrome.com/blog/chrome-devtools-mcp-debug-your-browser-session)
- [Chrome DevTools MCP README](https://github.com/ChromeDevTools/chrome-devtools-mcp) - [Chrome DevTools MCP README](https://github.com/ChromeDevTools/chrome-devtools-mcp)
Create a profile: Built-in profile:
```bash - `user`
openclaw browser create-profile \
--name chrome-live \ Optional: create your own custom existing-session profile if you want a
--driver existing-session \ different name or color.
--color "#00AA00"
```
Then in Chrome: Then in Chrome:
@ -380,10 +384,10 @@ Then in Chrome:
Live attach smoke test: Live attach smoke test:
```bash ```bash
openclaw browser --browser-profile chrome-live start openclaw browser --browser-profile user start
openclaw browser --browser-profile chrome-live status openclaw browser --browser-profile user status
openclaw browser --browser-profile chrome-live tabs openclaw browser --browser-profile user tabs
openclaw browser --browser-profile chrome-live snapshot --format ai openclaw browser --browser-profile user snapshot --format ai
``` ```
What success looks like: What success looks like:
@ -402,9 +406,10 @@ What to check if attach does not work:
Agent use: Agent use:
- Use `browserSession="user"` when you need the users logged-in browser state. - Use `profile="user"` when you need the users logged-in browser state.
- If you know the profile name, pass `profile="chrome-live"` (or your custom - If you use a custom existing-session profile, pass that explicit profile name.
existing-session profile). - Prefer `profile="user"` over `profile="chrome-relay"` unless the user
explicitly wants the extension / attach-tab flow.
- Only choose this mode when the user is at the computer to approve the attach - Only choose this mode when the user is at the computer to approve the attach
prompt. prompt.
- the Gateway or node host can spawn `npx chrome-devtools-mcp@latest --autoConnect` - the Gateway or node host can spawn `npx chrome-devtools-mcp@latest --autoConnect`
@ -432,7 +437,7 @@ WSL2 / cross-namespace example:
browser: { browser: {
enabled: true, enabled: true,
relayBindHost: "0.0.0.0", relayBindHost: "0.0.0.0",
defaultProfile: "chrome", defaultProfile: "chrome-relay",
}, },
} }
``` ```

View File

@ -62,7 +62,7 @@ After upgrading OpenClaw:
## Use it (set gateway token once) ## Use it (set gateway token once)
OpenClaw ships with a built-in browser profile named `chrome` that targets the extension relay on the default port. OpenClaw ships with a built-in browser profile named `chrome-relay` that targets the extension relay on the default port.
Before first attach, open extension Options and set: Before first attach, open extension Options and set:
@ -71,8 +71,8 @@ Before first attach, open extension Options and set:
Use it: Use it:
- CLI: `openclaw browser --browser-profile chrome tabs` - CLI: `openclaw browser --browser-profile chrome-relay tabs`
- Agent tool: `browser` with `profile="chrome"` - Agent tool: `browser` with `profile="chrome-relay"`
If you want a different name or a different relay port, create your own profile: If you want a different name or a different relay port, create your own profile:

View File

@ -310,17 +310,16 @@ Profile management:
Common parameters: Common parameters:
- `browserSession` (`agent` | `user`)
- `profile` (optional; defaults to `browser.defaultProfile`) - `profile` (optional; defaults to `browser.defaultProfile`)
- `target` (`sandbox` | `host` | `node`) - `target` (`sandbox` | `host` | `node`)
- `node` (optional; picks a specific node id/name) - `node` (optional; picks a specific node id/name)
Notes: Notes:
- Requires `browser.enabled=true` (default is `true`; set `false` to disable). - Requires `browser.enabled=true` (default is `true`; set `false` to disable).
- `browserSession="agent"` is the safe default: isolated OpenClaw-managed browser.
- `browserSession="user"` means the real local host browser. Use it only when existing logins/cookies matter and the user is present to click/approve any attach prompt.
- `browserSession="user"` is host-only; do not combine it with sandbox/node targets.
- All actions accept optional `profile` parameter for multi-instance support. - All actions accept optional `profile` parameter for multi-instance support.
- `profile` overrides `browserSession` when both are supplied. - Omit `profile` for the safe default: isolated OpenClaw-managed browser (`openclaw`).
- Use `profile="user"` for the real local host browser when existing logins/cookies matter and the user is present to click/approve any attach prompt.
- Use `profile="chrome-relay"` only for the Chrome extension / toolbar-button attach flow.
- `profile="user"` and `profile="chrome-relay"` are host-only; do not combine them with sandbox/node targets.
- When `profile` is omitted, uses `browser.defaultProfile` (defaults to `openclaw`). - When `profile` is omitted, uses `browser.defaultProfile` (defaults to `openclaw`).
- Profile names: lowercase alphanumeric + hyphens only (max 64 chars). - Profile names: lowercase alphanumeric + hyphens only (max 64 chars).
- Port range: 18800-18899 (~100 profiles max). - Port range: 18800-18899 (~100 profiles max).

View File

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

View File

@ -74,7 +74,7 @@ function formatConsoleToolResult(result: {
} }
function isChromeStaleTargetError(profile: string | undefined, err: unknown): boolean { function isChromeStaleTargetError(profile: string | undefined, err: unknown): boolean {
if (profile !== "chrome") { if (profile !== "chrome-relay" && profile !== "chrome") {
return false; return false;
} }
const msg = String(err); const msg = String(err);
@ -340,7 +340,7 @@ export async function executeActAction(params: {
); );
} }
throw new Error( 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 }, { cause: err },
); );
} }

View File

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

View File

@ -187,7 +187,6 @@ async function runSnapshotToolCall(params: {
refs?: "aria" | "dom"; refs?: "aria" | "dom";
maxChars?: number; maxChars?: number;
profile?: string; profile?: string;
browserSession?: "agent" | "user";
}) { }) {
const tool = createBrowserTool(); const tool = createBrowserTool();
await tool.execute?.("call-1", { action: "snapshot", ...params }); await tool.execute?.("call-1", { action: "snapshot", ...params });
@ -288,58 +287,56 @@ describe("browser tool snapshot maxChars", () => {
expect(opts?.mode).toBeUndefined(); expect(opts?.mode).toBeUndefined();
}); });
it("defaults to host when using profile=chrome (even in sandboxed sessions)", async () => { it("defaults to host when using profile=chrome-relay (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 () => {
setResolvedBrowserProfiles({ setResolvedBrowserProfiles({
openclaw: { cdpPort: 18800, color: "#FF4500" }, "chrome-relay": {
chrome: { driver: "extension", cdpUrl: "http://127.0.0.1:18792", color: "#0066CC" }, driver: "extension",
cdpUrl: "http://127.0.0.1:18792",
color: "#0066CC",
},
}); });
const tool = createBrowserTool({ sandboxBridgeUrl: "http://127.0.0.1:9999" }); const tool = createBrowserTool({ sandboxBridgeUrl: "http://127.0.0.1:9999" });
await tool.execute?.("call-1", { await tool.execute?.("call-1", {
action: "snapshot", action: "snapshot",
browserSession: "user", profile: "chrome-relay",
snapshotFormat: "ai", snapshotFormat: "ai",
}); });
expect(browserClientMocks.browserSnapshot).toHaveBeenCalledWith( expect(browserClientMocks.browserSnapshot).toHaveBeenCalledWith(
undefined, undefined,
expect.objectContaining({ 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({ setResolvedBrowserProfiles({
openclaw: { cdpPort: 18800, color: "#FF4500" },
"chrome-live": { driver: "existing-session", attachOnly: true, color: "#00AA00" }, "chrome-live": { driver: "existing-session", attachOnly: true, color: "#00AA00" },
}); });
const tool = createBrowserTool({ sandboxBridgeUrl: "http://127.0.0.1:9999" }); const tool = createBrowserTool({ sandboxBridgeUrl: "http://127.0.0.1:9999" });
await tool.execute?.("call-1", { await tool.execute?.("call-1", {
action: "snapshot", action: "snapshot",
browserSession: "user", profile: "chrome-live",
snapshotFormat: "ai", 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({ setResolvedBrowserProfiles({
openclaw: { cdpPort: 18800, color: "#FF4500" }, user: { driver: "existing-session", attachOnly: true, color: "#00AA00" },
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" },
}); });
const tool = createBrowserTool({ sandboxBridgeUrl: "http://127.0.0.1:9999" }); const tool = createBrowserTool({ sandboxBridgeUrl: "http://127.0.0.1:9999" });
await expect( await expect(
tool.execute?.("call-1", { tool.execute?.("call-1", {
action: "snapshot", action: "snapshot",
browserSession: "user", profile: "user",
target: "sandbox", target: "sandbox",
snapshotFormat: "ai", 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 () => { it("lets the server choose snapshot format when the user does not request one", async () => {
const tool = createBrowserTool(); 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( expect(browserClientMocks.browserSnapshot).toHaveBeenCalledWith(
undefined, undefined,
expect.objectContaining({ expect.objectContaining({
profile: "chrome", profile: "chrome-relay",
}), }),
); );
const opts = browserClientMocks.browserSnapshot.mock.calls.at(-1)?.[1] as const opts = browserClientMocks.browserSnapshot.mock.calls.at(-1)?.[1] as
@ -458,14 +438,21 @@ describe("browser tool snapshot maxChars", () => {
expect(gatewayMocks.callGatewayTool).not.toHaveBeenCalled(); 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(); mockSingleBrowserProxyNode();
setResolvedBrowserProfiles({
"chrome-relay": {
driver: "extension",
cdpUrl: "http://127.0.0.1:18792",
color: "#0066CC",
},
});
const tool = createBrowserTool(); 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( expect(browserClientMocks.browserStatus).toHaveBeenCalledWith(
undefined, undefined,
expect.objectContaining({ profile: "chrome" }), expect.objectContaining({ profile: "chrome-relay" }),
); );
expect(gatewayMocks.callGatewayTool).not.toHaveBeenCalled(); expect(gatewayMocks.callGatewayTool).not.toHaveBeenCalled();
}); });
@ -758,7 +745,7 @@ describe("browser tool external content wrapping", () => {
describe("browser tool act stale target recovery", () => { describe("browser tool act stale target recovery", () => {
registerBrowserToolAfterEachReset(); 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 browserActionsMocks.browserAct
.mockRejectedValueOnce(new Error("404: tab not found")) .mockRejectedValueOnce(new Error("404: tab not found"))
.mockResolvedValueOnce({ ok: true }); .mockResolvedValueOnce({ ok: true });
@ -767,7 +754,7 @@ describe("browser tool act stale target recovery", () => {
const tool = createBrowserTool(); const tool = createBrowserTool();
const result = await tool.execute?.("call-1", { const result = await tool.execute?.("call-1", {
action: "act", action: "act",
profile: "chrome", profile: "chrome-relay",
request: { request: {
kind: "hover", kind: "hover",
targetId: "stale-tab", targetId: "stale-tab",
@ -780,18 +767,18 @@ describe("browser tool act stale target recovery", () => {
1, 1,
undefined, undefined,
expect.objectContaining({ targetId: "stale-tab", kind: "hover", ref: "btn-1" }), expect.objectContaining({ targetId: "stale-tab", kind: "hover", ref: "btn-1" }),
expect.objectContaining({ profile: "chrome" }), expect.objectContaining({ profile: "chrome-relay" }),
); );
expect(browserActionsMocks.browserAct).toHaveBeenNthCalledWith( expect(browserActionsMocks.browserAct).toHaveBeenNthCalledWith(
2, 2,
undefined, undefined,
expect.not.objectContaining({ targetId: expect.anything() }), expect.not.objectContaining({ targetId: expect.anything() }),
expect.objectContaining({ profile: "chrome" }), expect.objectContaining({ profile: "chrome-relay" }),
); );
expect(result?.details).toMatchObject({ ok: true }); 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")); browserActionsMocks.browserAct.mockRejectedValueOnce(new Error("404: tab not found"));
browserClientMocks.browserTabs.mockResolvedValueOnce([{ targetId: "only-tab" }]); browserClientMocks.browserTabs.mockResolvedValueOnce([{ targetId: "only-tab" }]);
@ -799,14 +786,14 @@ describe("browser tool act stale target recovery", () => {
await expect( await expect(
tool.execute?.("call-1", { tool.execute?.("call-1", {
action: "act", action: "act",
profile: "chrome", profile: "chrome-relay",
request: { request: {
kind: "click", kind: "click",
targetId: "stale-tab", targetId: "stale-tab",
ref: "btn-1", 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); expect(browserActionsMocks.browserAct).toHaveBeenCalledTimes(1);
}); });

View File

@ -17,7 +17,6 @@ import {
browserStop, browserStop,
} from "../../browser/client.js"; } from "../../browser/client.js";
import { resolveBrowserConfig, resolveProfile } from "../../browser/config.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 { DEFAULT_UPLOAD_DIR, resolveExistingPathsWithinRoot } from "../../browser/paths.js";
import { getBrowserProfileCapabilities } from "../../browser/profile-capabilities.js"; import { getBrowserProfileCapabilities } from "../../browser/profile-capabilities.js";
import { applyBrowserProxyPaths, persistBrowserProxyFiles } from "../../browser/proxy-files.js"; import { applyBrowserProxyPaths, persistBrowserProxyFiles } from "../../browser/proxy-files.js";
@ -280,58 +279,22 @@ function resolveBrowserBaseUrl(params: {
return undefined; return undefined;
} }
function listUserBrowserProfiles() { function shouldPreferHostForProfile(profileName: string | undefined) {
if (!profileName) {
return false;
}
const cfg = loadConfig(); const cfg = loadConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg); const resolved = resolveBrowserConfig(cfg.browser, cfg);
return Object.keys(resolved.profiles ?? {}) const profile = resolveProfile(resolved, profileName);
.map((name) => resolveProfile(resolved, name)) if (!profile) {
.filter((profile): profile is NonNullable<typeof profile> => Boolean(profile)) return false;
.filter((profile) => { }
const capabilities = getBrowserProfileCapabilities(profile); const capabilities = getBrowserProfileCapabilities(profile);
return capabilities.requiresRelay || capabilities.usesChromeMcp; return capabilities.requiresRelay || capabilities.usesChromeMcp;
});
} }
function resolveBrowserToolProfile(params: { function isHostOnlyProfileName(profileName: string | undefined) {
profile?: string; return profileName === "user" || profileName === "chrome-relay";
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>".`,
);
} }
export function createBrowserTool(opts?: { export function createBrowserTool(opts?: {
@ -347,12 +310,12 @@ export function createBrowserTool(opts?: {
name: "browser", name: "browser",
description: [ description: [
"Control the browser via OpenClaw's browser control server (status/start/stop/profiles/tabs/open/snapshot/screenshot/actions).", "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.', "Browser choice: omit profile by default for the isolated OpenClaw-managed browser (`openclaw`).",
'browserSession="user" means the real local user browser on the host, not sandbox/node browsers. If user presence is unclear, ask first.', '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.',
'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.', '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 use browserSession="user" and prefer profile="chrome" (do not ask which profile unless ambiguous).', '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".', '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).", "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.', '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.", "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) => { execute: async (_toolCallId, args) => {
const params = args as Record<string, unknown>; const params = args as Record<string, unknown>;
const action = readStringParam(params, "action", { required: true }); const action = readStringParam(params, "action", { required: true });
const browserSession = readStringParam(params, "browserSession") as const profile = readStringParam(params, "profile");
| "agent"
| "user"
| undefined;
const profile = resolveBrowserToolProfile({
profile: readStringParam(params, "profile"),
browserSession,
});
const requestedNode = readStringParam(params, "node"); const requestedNode = readStringParam(params, "node");
let target = readStringParam(params, "target") as "sandbox" | "host" | "node" | undefined; let target = readStringParam(params, "target") as "sandbox" | "host" | "node" | undefined;
if (requestedNode && target && target !== "node") { if (requestedNode && target && target !== "node") {
throw new Error('node is only supported with target="node".'); throw new Error('node is only supported with target="node".');
} }
if (browserSession === "user") { if (isHostOnlyProfileName(profile)) {
if (requestedNode || target === "node") { 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") { if (target === "sandbox") {
throw new Error( 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") { if (!target && !requestedNode && shouldPreferHostForProfile(profile)) {
target = "host"; // Local host user-browser profiles should not silently bind to sandbox/node browsers.
}
if (!target && !requestedNode && profile === "chrome") {
// Chrome extension relay takeover is a host Chrome feature; prefer host unless explicitly targeting a node.
target = "host"; target = "host";
} }

View File

@ -22,10 +22,14 @@ describe("browser config", () => {
expect(openclaw?.driver).toBe("openclaw"); expect(openclaw?.driver).toBe("openclaw");
expect(openclaw?.cdpPort).toBe(18800); expect(openclaw?.cdpPort).toBe(18800);
expect(openclaw?.cdpUrl).toBe("http://127.0.0.1:18800"); expect(openclaw?.cdpUrl).toBe("http://127.0.0.1:18800");
const chrome = resolveProfile(resolved, "chrome"); const user = resolveProfile(resolved, "user");
expect(chrome?.driver).toBe("extension"); expect(user?.driver).toBe("existing-session");
expect(chrome?.cdpPort).toBe(18792); expect(user?.cdpPort).toBe(0);
expect(chrome?.cdpUrl).toBe("http://127.0.0.1:18792"); 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.remoteCdpTimeoutMs).toBe(1500);
expect(resolved.remoteCdpHandshakeTimeoutMs).toBe(3000); expect(resolved.remoteCdpHandshakeTimeoutMs).toBe(3000);
}); });
@ -34,10 +38,10 @@ describe("browser config", () => {
withEnv({ OPENCLAW_GATEWAY_PORT: "19001" }, () => { withEnv({ OPENCLAW_GATEWAY_PORT: "19001" }, () => {
const resolved = resolveBrowserConfig(undefined); const resolved = resolveBrowserConfig(undefined);
expect(resolved.controlPort).toBe(19003); expect(resolved.controlPort).toBe(19003);
const chrome = resolveProfile(resolved, "chrome"); const chromeRelay = resolveProfile(resolved, "chrome-relay");
expect(chrome?.driver).toBe("extension"); expect(chromeRelay?.driver).toBe("extension");
expect(chrome?.cdpPort).toBe(19004); expect(chromeRelay?.cdpPort).toBe(19004);
expect(chrome?.cdpUrl).toBe("http://127.0.0.1:19004"); expect(chromeRelay?.cdpUrl).toBe("http://127.0.0.1:19004");
const openclaw = resolveProfile(resolved, "openclaw"); const openclaw = resolveProfile(resolved, "openclaw");
expect(openclaw?.cdpPort).toBe(19012); expect(openclaw?.cdpPort).toBe(19012);
@ -49,10 +53,10 @@ describe("browser config", () => {
withEnv({ OPENCLAW_GATEWAY_PORT: undefined }, () => { withEnv({ OPENCLAW_GATEWAY_PORT: undefined }, () => {
const resolved = resolveBrowserConfig(undefined, { gateway: { port: 19011 } }); const resolved = resolveBrowserConfig(undefined, { gateway: { port: 19011 } });
expect(resolved.controlPort).toBe(19013); expect(resolved.controlPort).toBe(19013);
const chrome = resolveProfile(resolved, "chrome"); const chromeRelay = resolveProfile(resolved, "chrome-relay");
expect(chrome?.driver).toBe("extension"); expect(chromeRelay?.driver).toBe("extension");
expect(chrome?.cdpPort).toBe(19014); expect(chromeRelay?.cdpPort).toBe(19014);
expect(chrome?.cdpUrl).toBe("http://127.0.0.1:19014"); expect(chromeRelay?.cdpUrl).toBe("http://127.0.0.1:19014");
const openclaw = resolveProfile(resolved, "openclaw"); const openclaw = resolveProfile(resolved, "openclaw");
expect(openclaw?.cdpPort).toBe(19022); 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({ const resolved = resolveBrowserConfig({
profiles: { profiles: {
openclaw: { cdpPort: 18792, color: "#FF4500" }, openclaw: { cdpPort: 18792, color: "#FF4500" },
}, },
}); });
expect(resolveProfile(resolved, "chrome")).toBe(null); expect(resolveProfile(resolved, "chrome-relay")).toBe(null);
expect(resolved.defaultProfile).toBe("openclaw"); expect(resolved.defaultProfile).toBe("openclaw");
}); });
@ -313,7 +317,7 @@ describe("browser config", () => {
const managed = resolveProfile(resolved, "openclaw")!; const managed = resolveProfile(resolved, "openclaw")!;
expect(getBrowserProfileCapabilities(managed).usesChromeMcp).toBe(false); expect(getBrowserProfileCapabilities(managed).usesChromeMcp).toBe(false);
const extension = resolveProfile(resolved, "chrome")!; const extension = resolveProfile(resolved, "chrome-relay")!;
expect(getBrowserProfileCapabilities(extension).usesChromeMcp).toBe(false); expect(getBrowserProfileCapabilities(extension).usesChromeMcp).toBe(false);
const work = resolveProfile(resolved, "work")!; const work = resolveProfile(resolved, "work")!;
@ -354,17 +358,17 @@ describe("browser config", () => {
it("explicit defaultProfile config overrides defaults in headless mode", () => { it("explicit defaultProfile config overrides defaults in headless mode", () => {
const resolved = resolveBrowserConfig({ const resolved = resolveBrowserConfig({
headless: true, 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", () => { it("explicit defaultProfile config overrides defaults in noSandbox mode", () => {
const resolved = resolveBrowserConfig({ const resolved = resolveBrowserConfig({
noSandbox: true, 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", () => { 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. * Note: this is an OpenClaw browser profile (routing config), not a Chrome user profile.
* It points at the local relay CDP endpoint (controlPort + 1). * It points at the local relay CDP endpoint (controlPort + 1).
*/ */
function ensureDefaultChromeExtensionProfile( function ensureDefaultChromeRelayProfile(
profiles: Record<string, BrowserProfileConfig>, profiles: Record<string, BrowserProfileConfig>,
controlPort: number, controlPort: number,
): Record<string, BrowserProfileConfig> { ): Record<string, BrowserProfileConfig> {
const result = { ...profiles }; const result = { ...profiles };
if (result.chrome) { if (result["chrome-relay"]) {
return result; return result;
} }
const relayPort = controlPort + 1; const relayPort = controlPort + 1;
@ -202,7 +220,7 @@ function ensureDefaultChromeExtensionProfile(
if (getUsedPorts(result).has(relayPort)) { if (getUsedPorts(result).has(relayPort)) {
return result; return result;
} }
result.chrome = { result["chrome-relay"] = {
driver: "extension", driver: "extension",
cdpUrl: `http://127.0.0.1:${relayPort}`, cdpUrl: `http://127.0.0.1:${relayPort}`,
color: "#00AA00", color: "#00AA00",
@ -268,13 +286,15 @@ export function resolveBrowserConfig(
const legacyCdpPort = rawCdpUrl ? cdpInfo.port : undefined; const legacyCdpPort = rawCdpUrl ? cdpInfo.port : undefined;
const isWsUrl = cdpInfo.parsed.protocol === "ws:" || cdpInfo.parsed.protocol === "wss:"; const isWsUrl = cdpInfo.parsed.protocol === "ws:" || cdpInfo.parsed.protocol === "wss:";
const legacyCdpUrl = rawCdpUrl && isWsUrl ? cdpInfo.normalized : undefined; const legacyCdpUrl = rawCdpUrl && isWsUrl ? cdpInfo.normalized : undefined;
const profiles = ensureDefaultChromeExtensionProfile( const profiles = ensureDefaultChromeRelayProfile(
ensureDefaultProfile( ensureDefaultUserBrowserProfile(
cfg?.profiles, ensureDefaultProfile(
defaultColor, cfg?.profiles,
legacyCdpPort, defaultColor,
cdpPortRangeStart, legacyCdpPort,
legacyCdpUrl, cdpPortRangeStart,
legacyCdpUrl,
),
), ),
controlPort, controlPort,
); );
@ -286,7 +306,7 @@ export function resolveBrowserConfig(
? DEFAULT_BROWSER_DEFAULT_PROFILE_NAME ? DEFAULT_BROWSER_DEFAULT_PROFILE_NAME
: profiles[DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME] : profiles[DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME]
? DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME ? DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME
: "chrome"); : "user");
const extraArgs = Array.isArray(cfg?.extraArgs) const extraArgs = Array.isArray(cfg?.extraArgs)
? cfg.extraArgs.filter((a): a is string => typeof a === "string" && a.trim().length > 0) ? 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"; import { resolveSnapshotPlan } from "./agent.snapshot.plan.js";
describe("resolveSnapshotPlan", () => { 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 resolved = resolveBrowserConfig({});
const profile = resolveProfile(resolved, "chrome"); const profile = resolveProfile(resolved, "chrome-relay");
expect(profile).toBeTruthy(); expect(profile).toBeTruthy();
const plan = resolveSnapshotPlan({ const plan = resolveSnapshotPlan({

View File

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

View File

@ -43,7 +43,7 @@ describe("ensureExtensionRelayForProfiles", () => {
it("starts relay only for extension profiles", async () => { it("starts relay only for extension profiles", async () => {
resolveProfileMock.mockImplementation((_resolved: unknown, name: string) => { 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: "extension", cdpUrl: "http://127.0.0.1:18888" };
} }
return { driver: "openclaw", cdpUrl: "http://127.0.0.1:18889" }; return { driver: "openclaw", cdpUrl: "http://127.0.0.1:18889" };
@ -53,7 +53,7 @@ describe("ensureExtensionRelayForProfiles", () => {
await ensureExtensionRelayForProfiles({ await ensureExtensionRelayForProfiles({
resolved: { resolved: {
profiles: { profiles: {
chrome: {}, "chrome-relay": {},
openclaw: {}, openclaw: {},
}, },
} as never, } as never,
@ -72,12 +72,12 @@ describe("ensureExtensionRelayForProfiles", () => {
const onWarn = vi.fn(); const onWarn = vi.fn();
await ensureExtensionRelayForProfiles({ await ensureExtensionRelayForProfiles({
resolved: { profiles: { chrome: {} } } as never, resolved: { profiles: { "chrome-relay": {} } } as never,
onWarn, onWarn,
}); });
expect(onWarn).toHaveBeenCalledWith( 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 () => { 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>> = { const stopMap: Record<string, ReturnType<typeof vi.fn>> = {
openclaw: vi.fn(async () => {}), openclaw: vi.fn(async () => {}),
chrome: vi.fn(async () => { "chrome-relay": vi.fn(async () => {
throw new Error("profile stop failed"); throw new Error("profile stop failed");
}), }),
}; };
@ -112,7 +112,7 @@ describe("stopKnownBrowserProfiles", () => {
}); });
expect(stopMap.openclaw).toHaveBeenCalledTimes(1); expect(stopMap.openclaw).toHaveBeenCalledTimes(1);
expect(stopMap.chrome).toHaveBeenCalledTimes(1); expect(stopMap["chrome-relay"]).toHaveBeenCalledTimes(1);
expect(onWarn).not.toHaveBeenCalled(); expect(onWarn).not.toHaveBeenCalled();
}); });

View File

@ -22,7 +22,7 @@ const configMocks = vi.hoisted(() => ({
const browserConfigMocks = vi.hoisted(() => ({ const browserConfigMocks = vi.hoisted(() => ({
resolveBrowserConfig: vi.fn(() => ({ resolveBrowserConfig: vi.fn(() => ({
enabled: true, enabled: true,
defaultProfile: "chrome", defaultProfile: "openclaw",
})), })),
})); }));
@ -45,7 +45,7 @@ describe("runBrowserProxyCommand", () => {
}); });
browserConfigMocks.resolveBrowserConfig.mockReturnValue({ browserConfigMocks.resolveBrowserConfig.mockReturnValue({
enabled: true, enabled: true,
defaultProfile: "chrome", defaultProfile: "openclaw",
}); });
controlServiceMocks.startBrowserControlServiceFromConfig.mockResolvedValue(true); controlServiceMocks.startBrowserControlServiceFromConfig.mockResolvedValue(true);
}); });
@ -70,12 +70,12 @@ describe("runBrowserProxyCommand", () => {
JSON.stringify({ JSON.stringify({
method: "GET", method: "GET",
path: "/snapshot", path: "/snapshot",
profile: "chrome", profile: "chrome-relay",
timeoutMs: 5, timeoutMs: 5,
}), }),
), ),
).rejects.toThrow( ).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({ JSON.stringify({
method: "GET", method: "GET",
path: "/snapshot", path: "/snapshot",
profile: "chrome-live", profile: "user",
timeoutMs: 5, timeoutMs: 5,
}), }),
), ),
).rejects.toThrow( ).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({ JSON.stringify({
method: "POST", method: "POST",
path: "/act", path: "/act",
profile: "chrome", profile: "chrome-relay",
timeoutMs: 50, timeoutMs: 50,
}), }),
), ),