mirror of
https://fastgit.cc/github.com/openclaw/openclaw
synced 2026-04-20 21:02:10 +08:00
browser: route existing-session user profile through browser nodes (#68891)
* browser: route user profile through browser nodes * browser: align existing-session node docs * browser: preserve host fallback on node discovery errors * browser: preserve configured node pin errors * browser: widen config mock in node pin test
This commit is contained in:
@@ -11,6 +11,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Control UI/cron: keep the runtime-only `last` delivery sentinel from being materialized into persisted cron delivery and failure-alert channel configs when jobs are created or edited. (#68829) Thanks @tianhaocui.
|
||||
- OpenAI/Responses: strip orphaned reasoning blocks before outbound Responses API calls so compacted or restored histories no longer fail on standalone reasoning items. (#55787) Thanks @suboss87.
|
||||
- Cron/CLI: parse PowerShell-style `--tools` allow-lists the same way as comma-separated input, so `cron add` and `cron edit` no longer persist `exec read write` as one combined tool entry on Windows. (#68858) Thanks @chen-zhang-cs-code.
|
||||
- Browser/user-profile: let existing-session `profile="user"` tool calls auto-route to a connected browser node or use explicit `target="node"`, while still honoring explicit `target="host"` pinning. (#48677)
|
||||
|
||||
## 2026.4.19-beta.2
|
||||
|
||||
|
||||
@@ -2967,7 +2967,8 @@ See [Plugins](/tools/plugin).
|
||||
- `profiles.*.cdpUrl` accepts `http://`, `https://`, `ws://`, and `wss://`.
|
||||
Use HTTP(S) when you want OpenClaw to discover `/json/version`; use WS(S)
|
||||
when your provider gives you a direct DevTools WebSocket URL.
|
||||
- `existing-session` profiles are host-only and use Chrome MCP instead of CDP.
|
||||
- `existing-session` profiles use Chrome MCP instead of CDP and can attach on
|
||||
the selected host or through a connected browser node.
|
||||
- `existing-session` profiles can set `userDataDir` to target a specific
|
||||
Chromium-based browser profile such as Brave or Edge.
|
||||
- `existing-session` profiles keep the current Chrome MCP route limits:
|
||||
|
||||
@@ -1258,7 +1258,7 @@ for usage/billing and raise limits as needed.
|
||||
openclaw browser --browser-profile chrome-live tabs
|
||||
```
|
||||
|
||||
This path is host-local. If the Gateway runs elsewhere, either run a node host on the browser machine or use remote CDP instead.
|
||||
This path can use the local host browser or a connected browser node. If the Gateway runs elsewhere, either run a node host on the browser machine or use remote CDP instead.
|
||||
|
||||
Current limits on `existing-session` / `user`:
|
||||
|
||||
|
||||
@@ -532,8 +532,9 @@ Notes:
|
||||
- Existing-session dialog hooks do not support timeout overrides.
|
||||
- Some features still require the managed browser path, including batch
|
||||
actions, PDF export, download interception, and `responsebody`.
|
||||
- Existing-session is host-local. If Chrome lives on a different machine or a
|
||||
different network namespace, use remote CDP or a node host instead.
|
||||
- Existing-session can attach on the selected host or through a connected
|
||||
browser node. If Chrome lives elsewhere and no browser node is connected, use
|
||||
remote CDP or a node host instead.
|
||||
|
||||
## Isolation guarantees
|
||||
|
||||
|
||||
@@ -113,7 +113,12 @@ const gatewayMocks = vi.hoisted(() => ({
|
||||
vi.mock("../../../src/agents/tools/gateway.js", () => gatewayMocks);
|
||||
|
||||
const configMocks = vi.hoisted(() => ({
|
||||
loadConfig: vi.fn(() => ({ browser: {} })),
|
||||
loadConfig: vi.fn<
|
||||
() => {
|
||||
browser: Record<string, unknown>;
|
||||
gateway?: { nodes?: { browser?: { node?: string } } };
|
||||
}
|
||||
>(() => ({ browser: {} })),
|
||||
}));
|
||||
vi.mock("openclaw/plugin-sdk/config-runtime", async () => {
|
||||
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/config-runtime")>(
|
||||
@@ -340,7 +345,7 @@ describe("browser tool snapshot maxChars", () => {
|
||||
expect(opts?.mode).toBeUndefined();
|
||||
});
|
||||
|
||||
it("defaults to host when using profile=user (even in sandboxed sessions)", async () => {
|
||||
it("keeps profile=user off the sandbox browser when no node is selected", async () => {
|
||||
setResolvedBrowserProfiles({
|
||||
user: { driver: "existing-session", attachOnly: true, color: "#00AA00" },
|
||||
});
|
||||
@@ -360,7 +365,7 @@ describe("browser tool snapshot maxChars", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("defaults to host for custom existing-session profiles too", async () => {
|
||||
it("keeps custom existing-session profiles off the sandbox browser too", async () => {
|
||||
setResolvedBrowserProfiles({
|
||||
"chrome-live": { driver: "existing-session", attachOnly: true, color: "#00AA00" },
|
||||
});
|
||||
@@ -470,7 +475,7 @@ describe("browser tool snapshot maxChars", () => {
|
||||
expect(gatewayMocks.callGatewayTool).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps user profile on host when node proxy is available", async () => {
|
||||
it("routes profile=user through the node proxy when one is available", async () => {
|
||||
mockSingleBrowserProxyNode();
|
||||
setResolvedBrowserProfiles({
|
||||
user: { driver: "existing-session", attachOnly: true, color: "#00AA00" },
|
||||
@@ -478,6 +483,113 @@ describe("browser tool snapshot maxChars", () => {
|
||||
const tool = createBrowserTool();
|
||||
await tool.execute?.("call-1", { action: "status", profile: "user" });
|
||||
|
||||
expect(gatewayMocks.callGatewayTool).toHaveBeenCalledWith(
|
||||
"node.invoke",
|
||||
{ timeoutMs: 25000 },
|
||||
expect.objectContaining({
|
||||
nodeId: "node-1",
|
||||
command: "browser.proxy",
|
||||
params: expect.objectContaining({
|
||||
profile: "user",
|
||||
path: "/",
|
||||
method: "GET",
|
||||
timeoutMs: 20000,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(browserClientMocks.browserStatus).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back to the host for profile=user when node discovery errors", async () => {
|
||||
nodesUtilsMocks.listNodes.mockRejectedValueOnce(new Error("gateway unavailable"));
|
||||
setResolvedBrowserProfiles({
|
||||
user: { driver: "existing-session", attachOnly: true, color: "#00AA00" },
|
||||
});
|
||||
const tool = createBrowserTool();
|
||||
await tool.execute?.("call-1", { action: "status", profile: "user" });
|
||||
|
||||
expect(browserClientMocks.browserStatus).toHaveBeenCalledWith(
|
||||
undefined,
|
||||
expect.objectContaining({ profile: "user" }),
|
||||
);
|
||||
expect(gatewayMocks.callGatewayTool).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("preserves configured node pins when profile=user node discovery errors", async () => {
|
||||
nodesUtilsMocks.listNodes.mockRejectedValueOnce(new Error("gateway unavailable"));
|
||||
configMocks.loadConfig.mockReturnValue({
|
||||
browser: {},
|
||||
gateway: { nodes: { browser: { node: "node-1" } } },
|
||||
});
|
||||
setResolvedBrowserProfiles({
|
||||
user: { driver: "existing-session", attachOnly: true, color: "#00AA00" },
|
||||
});
|
||||
const tool = createBrowserTool();
|
||||
|
||||
await expect(tool.execute?.("call-1", { action: "status", profile: "user" })).rejects.toThrow(
|
||||
/gateway unavailable/i,
|
||||
);
|
||||
|
||||
expect(browserClientMocks.browserStatus).not.toHaveBeenCalled();
|
||||
expect(gatewayMocks.callGatewayTool).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('allows profile="user" with target="node"', async () => {
|
||||
mockSingleBrowserProxyNode();
|
||||
setResolvedBrowserProfiles({
|
||||
user: { driver: "existing-session", attachOnly: true, color: "#00AA00" },
|
||||
});
|
||||
const tool = createBrowserTool();
|
||||
await tool.execute?.("call-1", { action: "status", profile: "user", target: "node" });
|
||||
|
||||
expect(gatewayMocks.callGatewayTool).toHaveBeenCalledWith(
|
||||
"node.invoke",
|
||||
{ timeoutMs: 25000 },
|
||||
expect.objectContaining({
|
||||
nodeId: "node-1",
|
||||
command: "browser.proxy",
|
||||
params: expect.objectContaining({
|
||||
profile: "user",
|
||||
path: "/",
|
||||
method: "GET",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(browserClientMocks.browserStatus).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('allows profile="user" with an explicit node pin', async () => {
|
||||
mockSingleBrowserProxyNode();
|
||||
setResolvedBrowserProfiles({
|
||||
user: { driver: "existing-session", attachOnly: true, color: "#00AA00" },
|
||||
});
|
||||
const tool = createBrowserTool();
|
||||
await tool.execute?.("call-1", { action: "status", profile: "user", node: "node-1" });
|
||||
|
||||
expect(gatewayMocks.callGatewayTool).toHaveBeenCalledWith(
|
||||
"node.invoke",
|
||||
{ timeoutMs: 25000 },
|
||||
expect.objectContaining({
|
||||
nodeId: "node-1",
|
||||
command: "browser.proxy",
|
||||
params: expect.objectContaining({
|
||||
profile: "user",
|
||||
path: "/",
|
||||
method: "GET",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(browserClientMocks.browserStatus).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('keeps profile="user" on the host when target="host" is explicit', async () => {
|
||||
mockSingleBrowserProxyNode();
|
||||
setResolvedBrowserProfiles({
|
||||
user: { driver: "existing-session", attachOnly: true, color: "#00AA00" },
|
||||
});
|
||||
const tool = createBrowserTool();
|
||||
await tool.execute?.("call-1", { action: "status", profile: "user", target: "host" });
|
||||
|
||||
expect(browserClientMocks.browserStatus).toHaveBeenCalledWith(
|
||||
undefined,
|
||||
expect.objectContaining({ profile: "user" }),
|
||||
|
||||
@@ -380,7 +380,7 @@ 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, use profile="user". A supported Chromium-based browser (v144+) must be running. Use only when existing logins/cookies matter and the user is present.',
|
||||
'For the logged-in user browser, use profile="user". A supported Chromium-based browser (v144+) must be running on the selected host or browser node. 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".',
|
||||
"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.',
|
||||
@@ -395,31 +395,39 @@ export function createBrowserTool(opts?: {
|
||||
const profile = readStringParam(params, "profile");
|
||||
const requestedNode = readStringParam(params, "node");
|
||||
let target = readStringParam(params, "target") as "sandbox" | "host" | "node" | undefined;
|
||||
const configuredNode = browserToolDeps.loadConfig().gateway?.nodes?.browser?.node?.trim();
|
||||
|
||||
if (requestedNode && target && target !== "node") {
|
||||
throw new Error('node is only supported with target="node".');
|
||||
}
|
||||
// User-browser profiles (existing-session) are host-only.
|
||||
// existing-session profiles can attach through the selected host or browser node,
|
||||
// but they must never fall back into the sandbox browser.
|
||||
const isUserBrowserProfile = shouldPreferHostForProfile(profile);
|
||||
if (isUserBrowserProfile) {
|
||||
if (requestedNode || target === "node") {
|
||||
throw new Error(`profile="${profile}" only supports the local host browser.`);
|
||||
}
|
||||
if (target === "sandbox") {
|
||||
throw new Error(
|
||||
`profile="${profile}" cannot use the sandbox browser; use target="host" or omit target.`,
|
||||
);
|
||||
}
|
||||
if (!target && !requestedNode) {
|
||||
target = "host";
|
||||
}
|
||||
}
|
||||
|
||||
const nodeTarget = await resolveBrowserNodeTarget({
|
||||
requestedNode: requestedNode ?? undefined,
|
||||
target,
|
||||
sandboxBridgeUrl: opts?.sandboxBridgeUrl,
|
||||
});
|
||||
let nodeTarget: BrowserNodeTarget | null = null;
|
||||
try {
|
||||
nodeTarget = await resolveBrowserNodeTarget({
|
||||
requestedNode: requestedNode ?? undefined,
|
||||
target,
|
||||
sandboxBridgeUrl: opts?.sandboxBridgeUrl,
|
||||
});
|
||||
} catch (error) {
|
||||
// Keep the logged-in user browser usable on the host when auto-discovery
|
||||
// of browser nodes fails transiently. Explicit node requests still fail.
|
||||
if (!(isUserBrowserProfile && !target && !requestedNode && !configuredNode)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
if (isUserBrowserProfile && !target && !requestedNode && !nodeTarget) {
|
||||
target = "host";
|
||||
}
|
||||
|
||||
const resolvedTarget = target === "node" ? undefined : target;
|
||||
const baseUrl = nodeTarget
|
||||
|
||||
@@ -671,7 +671,7 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
|
||||
type: "string",
|
||||
title: "Browser Profile User Data Dir",
|
||||
description:
|
||||
"Per-profile Chromium user data directory for existing-session attachment through Chrome DevTools MCP. Use this for host-local Brave, Edge, Chromium, or non-default Chrome profiles when the built-in auto-connect path would pick the wrong browser data directory.",
|
||||
"Per-profile Chromium user data directory for existing-session attachment through Chrome DevTools MCP. Use this for Brave, Edge, Chromium, or non-default Chrome profiles when the built-in auto-connect path would pick the wrong browser data directory on the selected host or browser node.",
|
||||
},
|
||||
driver: {
|
||||
anyOf: [
|
||||
@@ -690,7 +690,7 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
|
||||
],
|
||||
title: "Browser Profile Driver",
|
||||
description:
|
||||
'Per-profile browser driver mode. Use "openclaw" (or legacy "clawd") for CDP-based profiles, or use "existing-session" for host-local Chrome DevTools MCP attachment.',
|
||||
'Per-profile browser driver mode. Use "openclaw" (or legacy "clawd") for CDP-based profiles, or use "existing-session" for Chrome DevTools MCP attachment on the selected host or browser node.',
|
||||
},
|
||||
attachOnly: {
|
||||
type: "boolean",
|
||||
@@ -23583,12 +23583,12 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
|
||||
},
|
||||
"browser.profiles.*.userDataDir": {
|
||||
label: "Browser Profile User Data Dir",
|
||||
help: "Per-profile Chromium user data directory for existing-session attachment through Chrome DevTools MCP. Use this for host-local Brave, Edge, Chromium, or non-default Chrome profiles when the built-in auto-connect path would pick the wrong browser data directory.",
|
||||
help: "Per-profile Chromium user data directory for existing-session attachment through Chrome DevTools MCP. Use this for Brave, Edge, Chromium, or non-default Chrome profiles when the built-in auto-connect path would pick the wrong browser data directory on the selected host or browser node.",
|
||||
tags: ["storage"],
|
||||
},
|
||||
"browser.profiles.*.driver": {
|
||||
label: "Browser Profile Driver",
|
||||
help: 'Per-profile browser driver mode. Use "openclaw" (or legacy "clawd") for CDP-based profiles, or use "existing-session" for host-local Chrome DevTools MCP attachment.',
|
||||
help: 'Per-profile browser driver mode. Use "openclaw" (or legacy "clawd") for CDP-based profiles, or use "existing-session" for Chrome DevTools MCP attachment on the selected host or browser node.',
|
||||
tags: ["storage"],
|
||||
},
|
||||
"browser.profiles.*.attachOnly": {
|
||||
|
||||
@@ -279,9 +279,9 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
"browser.profiles.*.cdpUrl":
|
||||
"Per-profile CDP websocket URL used for explicit remote browser routing by profile name. Use this when profile connections terminate on remote hosts or tunnels.",
|
||||
"browser.profiles.*.userDataDir":
|
||||
"Per-profile Chromium user data directory for existing-session attachment through Chrome DevTools MCP. Use this for host-local Brave, Edge, Chromium, or non-default Chrome profiles when the built-in auto-connect path would pick the wrong browser data directory.",
|
||||
"Per-profile Chromium user data directory for existing-session attachment through Chrome DevTools MCP. Use this for Brave, Edge, Chromium, or non-default Chrome profiles when the built-in auto-connect path would pick the wrong browser data directory on the selected host or browser node.",
|
||||
"browser.profiles.*.driver":
|
||||
'Per-profile browser driver mode. Use "openclaw" (or legacy "clawd") for CDP-based profiles, or use "existing-session" for host-local Chrome DevTools MCP attachment.',
|
||||
'Per-profile browser driver mode. Use "openclaw" (or legacy "clawd") for CDP-based profiles, or use "existing-session" for Chrome DevTools MCP attachment on the selected host or browser node.',
|
||||
"browser.profiles.*.attachOnly":
|
||||
"Per-profile attach-only override that skips local browser launch and only attaches to an existing CDP endpoint. Useful when one profile is externally managed but others are locally launched.",
|
||||
"browser.profiles.*.color":
|
||||
|
||||
Reference in New Issue
Block a user