@agenticmail/enterprise 0.5.77 → 0.5.79
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-7RNT4O5T.js +15198 -0
- package/dist/chunk-AGFOJCSB.js +2191 -0
- package/dist/chunk-CYABMD5B.js +2191 -0
- package/dist/chunk-F4GSFCM3.js +898 -0
- package/dist/chunk-GINZ56GG.js +15035 -0
- package/dist/chunk-NRKB2KGD.js +898 -0
- package/dist/chunk-PZA7YOJE.js +898 -0
- package/dist/chunk-Q3V7VZFQ.js +2191 -0
- package/dist/chunk-RRFB6G6M.js +15198 -0
- package/dist/chunk-VX3VFMVB.js +409 -0
- package/dist/cli.js +1 -1
- package/dist/dashboard/pages/agent-detail.js +491 -2
- package/dist/index.js +4 -3
- package/dist/pw-ai-KPETTB25.js +2212 -0
- package/dist/routes-2T2ZNH3D.js +6642 -0
- package/dist/routes-PDHMCIXU.js +6676 -0
- package/dist/runtime-5ZJYB5PY.js +47 -0
- package/dist/runtime-7HW4GX5L.js +48 -0
- package/dist/runtime-XXDCZZIK.js +48 -0
- package/dist/server-FMP4BFGW.js +12 -0
- package/dist/server-JRHDUNII.js +12 -0
- package/dist/server-QPIMKFK4.js +12 -0
- package/dist/setup-NPFIX7LF.js +20 -0
- package/dist/setup-O5FPRLK4.js +20 -0
- package/dist/setup-S4Z4PPIJ.js +20 -0
- package/package.json +15 -2
- package/src/agent-tools/common.ts +25 -0
- package/src/agent-tools/index.ts +3 -0
- package/src/agent-tools/schema/typebox.ts +25 -0
- package/src/agent-tools/tools/browser-tool.schema.ts +112 -0
- package/src/agent-tools/tools/browser-tool.ts +388 -0
- package/src/agent-tools/tools/gateway.ts +126 -0
- package/src/agent-tools/tools/nodes-utils.ts +80 -0
- package/src/browser/bridge-auth-registry.ts +34 -0
- package/src/browser/bridge-server.ts +93 -0
- package/src/browser/cdp.helpers.ts +180 -0
- package/src/browser/cdp.ts +466 -0
- package/src/browser/chrome.executables.ts +625 -0
- package/src/browser/chrome.profile-decoration.ts +198 -0
- package/src/browser/chrome.ts +349 -0
- package/src/browser/client-actions-core.ts +259 -0
- package/src/browser/client-actions-observe.ts +184 -0
- package/src/browser/client-actions-state.ts +284 -0
- package/src/browser/client-actions-types.ts +16 -0
- package/src/browser/client-actions-url.ts +11 -0
- package/src/browser/client-actions.ts +4 -0
- package/src/browser/client-fetch.ts +253 -0
- package/src/browser/client.ts +337 -0
- package/src/browser/config.ts +296 -0
- package/src/browser/constants.ts +8 -0
- package/src/browser/control-auth.ts +94 -0
- package/src/browser/control-service.ts +81 -0
- package/src/browser/csrf.ts +87 -0
- package/src/browser/enterprise-compat.ts +518 -0
- package/src/browser/extension-relay.ts +834 -0
- package/src/browser/http-auth.ts +63 -0
- package/src/browser/navigation-guard.ts +50 -0
- package/src/browser/paths.ts +49 -0
- package/src/browser/profiles-service.ts +187 -0
- package/src/browser/profiles.ts +113 -0
- package/src/browser/proxy-files.ts +41 -0
- package/src/browser/pw-ai-module.ts +52 -0
- package/src/browser/pw-ai-state.ts +9 -0
- package/src/browser/pw-ai.ts +65 -0
- package/src/browser/pw-role-snapshot.ts +434 -0
- package/src/browser/pw-session.ts +810 -0
- package/src/browser/pw-tools-core.activity.ts +68 -0
- package/src/browser/pw-tools-core.downloads.ts +281 -0
- package/src/browser/pw-tools-core.interactions.ts +646 -0
- package/src/browser/pw-tools-core.responses.ts +124 -0
- package/src/browser/pw-tools-core.shared.ts +70 -0
- package/src/browser/pw-tools-core.snapshot.ts +213 -0
- package/src/browser/pw-tools-core.state.ts +209 -0
- package/src/browser/pw-tools-core.storage.ts +128 -0
- package/src/browser/pw-tools-core.trace.ts +37 -0
- package/src/browser/pw-tools-core.ts +8 -0
- package/src/browser/resolved-config-refresh.ts +59 -0
- package/src/browser/routes/agent.act.shared.ts +52 -0
- package/src/browser/routes/agent.act.ts +575 -0
- package/src/browser/routes/agent.debug.ts +149 -0
- package/src/browser/routes/agent.shared.ts +143 -0
- package/src/browser/routes/agent.snapshot.ts +333 -0
- package/src/browser/routes/agent.storage.ts +451 -0
- package/src/browser/routes/agent.ts +13 -0
- package/src/browser/routes/basic.ts +202 -0
- package/src/browser/routes/dispatcher.ts +126 -0
- package/src/browser/routes/index.ts +11 -0
- package/src/browser/routes/path-output.ts +1 -0
- package/src/browser/routes/tabs.ts +217 -0
- package/src/browser/routes/types.ts +26 -0
- package/src/browser/routes/utils.ts +73 -0
- package/src/browser/screenshot.ts +54 -0
- package/src/browser/server-context.ts +688 -0
- package/src/browser/server-context.types.ts +65 -0
- package/src/browser/server-lifecycle.ts +48 -0
- package/src/browser/server-middleware.ts +37 -0
- package/src/browser/server.ts +110 -0
- package/src/browser/target-id.ts +30 -0
- package/src/browser/trash.ts +21 -0
- package/src/dashboard/pages/agent-detail.js +491 -2
- package/src/engine/agent-routes.ts +246 -0
- package/src/security/external-content.ts +299 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_OPENCLAW_BROWSER_COLOR,
|
|
5
|
+
DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME,
|
|
6
|
+
} from "./constants.js";
|
|
7
|
+
|
|
8
|
+
function decoratedMarkerPath(userDataDir: string) {
|
|
9
|
+
return path.join(userDataDir, ".openclaw-profile-decorated");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function safeReadJson(filePath: string): Record<string, unknown> | null {
|
|
13
|
+
try {
|
|
14
|
+
if (!fs.existsSync(filePath)) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
18
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
19
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
return parsed as Record<string, unknown>;
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function safeWriteJson(filePath: string, data: Record<string, unknown>) {
|
|
29
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
30
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function setDeep(obj: Record<string, unknown>, keys: string[], value: unknown) {
|
|
34
|
+
let node: Record<string, unknown> = obj;
|
|
35
|
+
for (const key of keys.slice(0, -1)) {
|
|
36
|
+
const next = node[key];
|
|
37
|
+
if (typeof next !== "object" || next === null || Array.isArray(next)) {
|
|
38
|
+
node[key] = {};
|
|
39
|
+
}
|
|
40
|
+
node = node[key] as Record<string, unknown>;
|
|
41
|
+
}
|
|
42
|
+
node[keys[keys.length - 1] ?? ""] = value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseHexRgbToSignedArgbInt(hex: string): number | null {
|
|
46
|
+
const cleaned = hex.trim().replace(/^#/, "");
|
|
47
|
+
if (!/^[0-9a-fA-F]{6}$/.test(cleaned)) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
const rgb = Number.parseInt(cleaned, 16);
|
|
51
|
+
const argbUnsigned = (0xff << 24) | rgb;
|
|
52
|
+
// Chrome stores colors as signed 32-bit ints (SkColor).
|
|
53
|
+
return argbUnsigned > 0x7fffffff ? argbUnsigned - 0x1_0000_0000 : argbUnsigned;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function isProfileDecorated(
|
|
57
|
+
userDataDir: string,
|
|
58
|
+
desiredName: string,
|
|
59
|
+
desiredColorHex: string,
|
|
60
|
+
): boolean {
|
|
61
|
+
const desiredColorInt = parseHexRgbToSignedArgbInt(desiredColorHex);
|
|
62
|
+
|
|
63
|
+
const localStatePath = path.join(userDataDir, "Local State");
|
|
64
|
+
const preferencesPath = path.join(userDataDir, "Default", "Preferences");
|
|
65
|
+
|
|
66
|
+
const localState = safeReadJson(localStatePath);
|
|
67
|
+
const profile = localState?.profile;
|
|
68
|
+
const infoCache =
|
|
69
|
+
typeof profile === "object" && profile !== null && !Array.isArray(profile)
|
|
70
|
+
? (profile as Record<string, unknown>).info_cache
|
|
71
|
+
: null;
|
|
72
|
+
const info =
|
|
73
|
+
typeof infoCache === "object" &&
|
|
74
|
+
infoCache !== null &&
|
|
75
|
+
!Array.isArray(infoCache) &&
|
|
76
|
+
typeof (infoCache as Record<string, unknown>).Default === "object" &&
|
|
77
|
+
(infoCache as Record<string, unknown>).Default !== null &&
|
|
78
|
+
!Array.isArray((infoCache as Record<string, unknown>).Default)
|
|
79
|
+
? ((infoCache as Record<string, unknown>).Default as Record<string, unknown>)
|
|
80
|
+
: null;
|
|
81
|
+
|
|
82
|
+
const prefs = safeReadJson(preferencesPath);
|
|
83
|
+
const browserTheme = (() => {
|
|
84
|
+
const browser = prefs?.browser;
|
|
85
|
+
const theme =
|
|
86
|
+
typeof browser === "object" && browser !== null && !Array.isArray(browser)
|
|
87
|
+
? (browser as Record<string, unknown>).theme
|
|
88
|
+
: null;
|
|
89
|
+
return typeof theme === "object" && theme !== null && !Array.isArray(theme)
|
|
90
|
+
? (theme as Record<string, unknown>)
|
|
91
|
+
: null;
|
|
92
|
+
})();
|
|
93
|
+
|
|
94
|
+
const autogeneratedTheme = (() => {
|
|
95
|
+
const autogenerated = prefs?.autogenerated;
|
|
96
|
+
const theme =
|
|
97
|
+
typeof autogenerated === "object" && autogenerated !== null && !Array.isArray(autogenerated)
|
|
98
|
+
? (autogenerated as Record<string, unknown>).theme
|
|
99
|
+
: null;
|
|
100
|
+
return typeof theme === "object" && theme !== null && !Array.isArray(theme)
|
|
101
|
+
? (theme as Record<string, unknown>)
|
|
102
|
+
: null;
|
|
103
|
+
})();
|
|
104
|
+
|
|
105
|
+
const nameOk = typeof info?.name === "string" ? info.name === desiredName : true;
|
|
106
|
+
|
|
107
|
+
if (desiredColorInt == null) {
|
|
108
|
+
// If the user provided a non-#RRGGBB value, we can only do best-effort.
|
|
109
|
+
return nameOk;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const localSeedOk =
|
|
113
|
+
typeof info?.profile_color_seed === "number"
|
|
114
|
+
? info.profile_color_seed === desiredColorInt
|
|
115
|
+
: false;
|
|
116
|
+
|
|
117
|
+
const prefOk =
|
|
118
|
+
(typeof browserTheme?.user_color2 === "number" &&
|
|
119
|
+
browserTheme.user_color2 === desiredColorInt) ||
|
|
120
|
+
(typeof autogeneratedTheme?.color === "number" && autogeneratedTheme.color === desiredColorInt);
|
|
121
|
+
|
|
122
|
+
return nameOk && localSeedOk && prefOk;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Best-effort profile decoration (name + lobster-orange). Chrome preference keys
|
|
127
|
+
* vary by version; we keep this conservative and idempotent.
|
|
128
|
+
*/
|
|
129
|
+
export function decorateOpenClawProfile(
|
|
130
|
+
userDataDir: string,
|
|
131
|
+
opts?: { name?: string; color?: string },
|
|
132
|
+
) {
|
|
133
|
+
const desiredName = opts?.name ?? DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME;
|
|
134
|
+
const desiredColor = (opts?.color ?? DEFAULT_OPENCLAW_BROWSER_COLOR).toUpperCase();
|
|
135
|
+
const desiredColorInt = parseHexRgbToSignedArgbInt(desiredColor);
|
|
136
|
+
|
|
137
|
+
const localStatePath = path.join(userDataDir, "Local State");
|
|
138
|
+
const preferencesPath = path.join(userDataDir, "Default", "Preferences");
|
|
139
|
+
|
|
140
|
+
const localState = safeReadJson(localStatePath) ?? {};
|
|
141
|
+
// Common-ish shape: profile.info_cache.Default
|
|
142
|
+
setDeep(localState, ["profile", "info_cache", "Default", "name"], desiredName);
|
|
143
|
+
setDeep(localState, ["profile", "info_cache", "Default", "shortcut_name"], desiredName);
|
|
144
|
+
setDeep(localState, ["profile", "info_cache", "Default", "user_name"], desiredName);
|
|
145
|
+
// Color keys are best-effort (Chrome changes these frequently).
|
|
146
|
+
setDeep(localState, ["profile", "info_cache", "Default", "profile_color"], desiredColor);
|
|
147
|
+
setDeep(localState, ["profile", "info_cache", "Default", "user_color"], desiredColor);
|
|
148
|
+
if (desiredColorInt != null) {
|
|
149
|
+
// These are the fields Chrome actually uses for profile/avatar tinting.
|
|
150
|
+
setDeep(
|
|
151
|
+
localState,
|
|
152
|
+
["profile", "info_cache", "Default", "profile_color_seed"],
|
|
153
|
+
desiredColorInt,
|
|
154
|
+
);
|
|
155
|
+
setDeep(
|
|
156
|
+
localState,
|
|
157
|
+
["profile", "info_cache", "Default", "profile_highlight_color"],
|
|
158
|
+
desiredColorInt,
|
|
159
|
+
);
|
|
160
|
+
setDeep(
|
|
161
|
+
localState,
|
|
162
|
+
["profile", "info_cache", "Default", "default_avatar_fill_color"],
|
|
163
|
+
desiredColorInt,
|
|
164
|
+
);
|
|
165
|
+
setDeep(
|
|
166
|
+
localState,
|
|
167
|
+
["profile", "info_cache", "Default", "default_avatar_stroke_color"],
|
|
168
|
+
desiredColorInt,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
safeWriteJson(localStatePath, localState);
|
|
172
|
+
|
|
173
|
+
const prefs = safeReadJson(preferencesPath) ?? {};
|
|
174
|
+
setDeep(prefs, ["profile", "name"], desiredName);
|
|
175
|
+
setDeep(prefs, ["profile", "profile_color"], desiredColor);
|
|
176
|
+
setDeep(prefs, ["profile", "user_color"], desiredColor);
|
|
177
|
+
if (desiredColorInt != null) {
|
|
178
|
+
// Chrome refresh stores the autogenerated theme in these prefs (SkColor ints).
|
|
179
|
+
setDeep(prefs, ["autogenerated", "theme", "color"], desiredColorInt);
|
|
180
|
+
// User-selected browser theme color (pref name: browser.theme.user_color2).
|
|
181
|
+
setDeep(prefs, ["browser", "theme", "user_color2"], desiredColorInt);
|
|
182
|
+
}
|
|
183
|
+
safeWriteJson(preferencesPath, prefs);
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
fs.writeFileSync(decoratedMarkerPath(userDataDir), `${Date.now()}\n`, "utf-8");
|
|
187
|
+
} catch {
|
|
188
|
+
// ignore
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function ensureProfileCleanExit(userDataDir: string) {
|
|
193
|
+
const preferencesPath = path.join(userDataDir, "Default", "Preferences");
|
|
194
|
+
const prefs = safeReadJson(preferencesPath) ?? {};
|
|
195
|
+
setDeep(prefs, ["exit_type"], "Normal");
|
|
196
|
+
setDeep(prefs, ["exited_cleanly"], true);
|
|
197
|
+
safeWriteJson(preferencesPath, prefs);
|
|
198
|
+
}
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import WebSocket from "ws";
|
|
6
|
+
|
|
7
|
+
import { appendCdpPath } from "./cdp.helpers.js";
|
|
8
|
+
import { getHeadersWithAuth, normalizeCdpWsUrl } from "./cdp.js";
|
|
9
|
+
import {
|
|
10
|
+
type BrowserExecutable,
|
|
11
|
+
resolveBrowserExecutableForPlatform,
|
|
12
|
+
} from "./chrome.executables.js";
|
|
13
|
+
import {
|
|
14
|
+
decorateOpenClawProfile,
|
|
15
|
+
ensureProfileCleanExit,
|
|
16
|
+
isProfileDecorated,
|
|
17
|
+
} from "./chrome.profile-decoration.js";
|
|
18
|
+
import type { ResolvedBrowserConfig, ResolvedBrowserProfile } from "./config.js";
|
|
19
|
+
import { CONFIG_DIR, createSubsystemLogger, ensurePortAvailable } from "./enterprise-compat.js";
|
|
20
|
+
import {
|
|
21
|
+
DEFAULT_OPENCLAW_BROWSER_COLOR,
|
|
22
|
+
DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME,
|
|
23
|
+
} from "./constants.js";
|
|
24
|
+
|
|
25
|
+
const log = createSubsystemLogger("browser").child("chrome");
|
|
26
|
+
|
|
27
|
+
export type { BrowserExecutable } from "./chrome.executables.js";
|
|
28
|
+
export {
|
|
29
|
+
findChromeExecutableLinux,
|
|
30
|
+
findChromeExecutableMac,
|
|
31
|
+
findChromeExecutableWindows,
|
|
32
|
+
resolveBrowserExecutableForPlatform,
|
|
33
|
+
} from "./chrome.executables.js";
|
|
34
|
+
export {
|
|
35
|
+
decorateOpenClawProfile,
|
|
36
|
+
ensureProfileCleanExit,
|
|
37
|
+
isProfileDecorated,
|
|
38
|
+
} from "./chrome.profile-decoration.js";
|
|
39
|
+
|
|
40
|
+
function exists(filePath: string) {
|
|
41
|
+
try {
|
|
42
|
+
return fs.existsSync(filePath);
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type RunningChrome = {
|
|
49
|
+
pid: number;
|
|
50
|
+
exe: BrowserExecutable;
|
|
51
|
+
userDataDir: string;
|
|
52
|
+
cdpPort: number;
|
|
53
|
+
startedAt: number;
|
|
54
|
+
proc: ChildProcessWithoutNullStreams;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function resolveBrowserExecutable(resolved: ResolvedBrowserConfig): BrowserExecutable | null {
|
|
58
|
+
return resolveBrowserExecutableForPlatform(resolved, process.platform);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function resolveOpenClawUserDataDir(profileName = DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME) {
|
|
62
|
+
return path.join(CONFIG_DIR, "browser", profileName, "user-data");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function cdpUrlForPort(cdpPort: number) {
|
|
66
|
+
return `http://127.0.0.1:${cdpPort}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function isChromeReachable(cdpUrl: string, timeoutMs = 500): Promise<boolean> {
|
|
70
|
+
const version = await fetchChromeVersion(cdpUrl, timeoutMs);
|
|
71
|
+
return Boolean(version);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
type ChromeVersion = {
|
|
75
|
+
webSocketDebuggerUrl?: string;
|
|
76
|
+
Browser?: string;
|
|
77
|
+
"User-Agent"?: string;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
async function fetchChromeVersion(cdpUrl: string, timeoutMs = 500): Promise<ChromeVersion | null> {
|
|
81
|
+
const ctrl = new AbortController();
|
|
82
|
+
const t = setTimeout(ctrl.abort.bind(ctrl), timeoutMs);
|
|
83
|
+
try {
|
|
84
|
+
const versionUrl = appendCdpPath(cdpUrl, "/json/version");
|
|
85
|
+
const res = await fetch(versionUrl, {
|
|
86
|
+
signal: ctrl.signal,
|
|
87
|
+
headers: getHeadersWithAuth(versionUrl),
|
|
88
|
+
});
|
|
89
|
+
if (!res.ok) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
const data = (await res.json()) as ChromeVersion;
|
|
93
|
+
if (!data || typeof data !== "object") {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
return data;
|
|
97
|
+
} catch {
|
|
98
|
+
return null;
|
|
99
|
+
} finally {
|
|
100
|
+
clearTimeout(t);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function getChromeWebSocketUrl(
|
|
105
|
+
cdpUrl: string,
|
|
106
|
+
timeoutMs = 500,
|
|
107
|
+
): Promise<string | null> {
|
|
108
|
+
const version = await fetchChromeVersion(cdpUrl, timeoutMs);
|
|
109
|
+
const wsUrl = String(version?.webSocketDebuggerUrl ?? "").trim();
|
|
110
|
+
if (!wsUrl) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
return normalizeCdpWsUrl(wsUrl, cdpUrl);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function canOpenWebSocket(wsUrl: string, timeoutMs = 800): Promise<boolean> {
|
|
117
|
+
return await new Promise<boolean>((resolve) => {
|
|
118
|
+
const headers = getHeadersWithAuth(wsUrl);
|
|
119
|
+
const ws = new WebSocket(wsUrl, {
|
|
120
|
+
handshakeTimeout: timeoutMs,
|
|
121
|
+
...(Object.keys(headers).length ? { headers } : {}),
|
|
122
|
+
});
|
|
123
|
+
const timer = setTimeout(
|
|
124
|
+
() => {
|
|
125
|
+
try {
|
|
126
|
+
ws.terminate();
|
|
127
|
+
} catch {
|
|
128
|
+
// ignore
|
|
129
|
+
}
|
|
130
|
+
resolve(false);
|
|
131
|
+
},
|
|
132
|
+
Math.max(50, timeoutMs + 25),
|
|
133
|
+
);
|
|
134
|
+
ws.once("open", () => {
|
|
135
|
+
clearTimeout(timer);
|
|
136
|
+
try {
|
|
137
|
+
ws.close();
|
|
138
|
+
} catch {
|
|
139
|
+
// ignore
|
|
140
|
+
}
|
|
141
|
+
resolve(true);
|
|
142
|
+
});
|
|
143
|
+
ws.once("error", () => {
|
|
144
|
+
clearTimeout(timer);
|
|
145
|
+
resolve(false);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function isChromeCdpReady(
|
|
151
|
+
cdpUrl: string,
|
|
152
|
+
timeoutMs = 500,
|
|
153
|
+
handshakeTimeoutMs = 800,
|
|
154
|
+
): Promise<boolean> {
|
|
155
|
+
const wsUrl = await getChromeWebSocketUrl(cdpUrl, timeoutMs);
|
|
156
|
+
if (!wsUrl) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
return await canOpenWebSocket(wsUrl, handshakeTimeoutMs);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function launchOpenClawChrome(
|
|
163
|
+
resolved: ResolvedBrowserConfig,
|
|
164
|
+
profile: ResolvedBrowserProfile,
|
|
165
|
+
): Promise<RunningChrome> {
|
|
166
|
+
if (!profile.cdpIsLoopback) {
|
|
167
|
+
throw new Error(`Profile "${profile.name}" is remote; cannot launch local Chrome.`);
|
|
168
|
+
}
|
|
169
|
+
await ensurePortAvailable(profile.cdpPort);
|
|
170
|
+
|
|
171
|
+
const exe = resolveBrowserExecutable(resolved);
|
|
172
|
+
if (!exe) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
"No supported browser found (Chrome/Brave/Edge/Chromium on macOS, Linux, or Windows).",
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const userDataDir = resolveOpenClawUserDataDir(profile.name);
|
|
179
|
+
fs.mkdirSync(userDataDir, { recursive: true });
|
|
180
|
+
|
|
181
|
+
const needsDecorate = !isProfileDecorated(
|
|
182
|
+
userDataDir,
|
|
183
|
+
profile.name,
|
|
184
|
+
(profile.color ?? DEFAULT_OPENCLAW_BROWSER_COLOR).toUpperCase(),
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
// First launch to create preference files if missing, then decorate and relaunch.
|
|
188
|
+
const spawnOnce = () => {
|
|
189
|
+
const args: string[] = [
|
|
190
|
+
`--remote-debugging-port=${profile.cdpPort}`,
|
|
191
|
+
`--user-data-dir=${userDataDir}`,
|
|
192
|
+
"--no-first-run",
|
|
193
|
+
"--no-default-browser-check",
|
|
194
|
+
"--disable-sync",
|
|
195
|
+
"--disable-background-networking",
|
|
196
|
+
"--disable-component-update",
|
|
197
|
+
"--disable-features=Translate,MediaRouter",
|
|
198
|
+
"--disable-session-crashed-bubble",
|
|
199
|
+
"--hide-crash-restore-bubble",
|
|
200
|
+
"--password-store=basic",
|
|
201
|
+
];
|
|
202
|
+
|
|
203
|
+
if (resolved.headless) {
|
|
204
|
+
// Best-effort; older Chromes may ignore.
|
|
205
|
+
args.push("--headless=new");
|
|
206
|
+
args.push("--disable-gpu");
|
|
207
|
+
}
|
|
208
|
+
if (resolved.noSandbox) {
|
|
209
|
+
args.push("--no-sandbox");
|
|
210
|
+
args.push("--disable-setuid-sandbox");
|
|
211
|
+
}
|
|
212
|
+
if (process.platform === "linux") {
|
|
213
|
+
args.push("--disable-dev-shm-usage");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Stealth: hide navigator.webdriver from automation detection (#80)
|
|
217
|
+
args.push("--disable-blink-features=AutomationControlled");
|
|
218
|
+
|
|
219
|
+
// Append user-configured extra arguments (e.g., stealth flags, window size)
|
|
220
|
+
if (resolved.extraArgs.length > 0) {
|
|
221
|
+
args.push(...resolved.extraArgs);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Always open a blank tab to ensure a target exists.
|
|
225
|
+
args.push("about:blank");
|
|
226
|
+
|
|
227
|
+
return spawn(exe.path, args, {
|
|
228
|
+
stdio: "pipe",
|
|
229
|
+
env: {
|
|
230
|
+
...process.env,
|
|
231
|
+
// Reduce accidental sharing with the user's env.
|
|
232
|
+
HOME: os.homedir(),
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const startedAt = Date.now();
|
|
238
|
+
|
|
239
|
+
const localStatePath = path.join(userDataDir, "Local State");
|
|
240
|
+
const preferencesPath = path.join(userDataDir, "Default", "Preferences");
|
|
241
|
+
const needsBootstrap = !exists(localStatePath) || !exists(preferencesPath);
|
|
242
|
+
|
|
243
|
+
// If the profile doesn't exist yet, bootstrap it once so Chrome creates defaults.
|
|
244
|
+
// Then decorate (if needed) before the "real" run.
|
|
245
|
+
if (needsBootstrap) {
|
|
246
|
+
const bootstrap = spawnOnce();
|
|
247
|
+
const deadline = Date.now() + 10_000;
|
|
248
|
+
while (Date.now() < deadline) {
|
|
249
|
+
if (exists(localStatePath) && exists(preferencesPath)) {
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
bootstrap.kill("SIGTERM");
|
|
256
|
+
} catch {
|
|
257
|
+
// ignore
|
|
258
|
+
}
|
|
259
|
+
const exitDeadline = Date.now() + 5000;
|
|
260
|
+
while (Date.now() < exitDeadline) {
|
|
261
|
+
if (bootstrap.exitCode != null) {
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (needsDecorate) {
|
|
269
|
+
try {
|
|
270
|
+
decorateOpenClawProfile(userDataDir, {
|
|
271
|
+
name: profile.name,
|
|
272
|
+
color: profile.color,
|
|
273
|
+
});
|
|
274
|
+
log.info(`🦞 openclaw browser profile decorated (${profile.color})`);
|
|
275
|
+
} catch (err) {
|
|
276
|
+
log.warn(`openclaw browser profile decoration failed: ${String(err)}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
ensureProfileCleanExit(userDataDir);
|
|
282
|
+
} catch (err) {
|
|
283
|
+
log.warn(`openclaw browser clean-exit prefs failed: ${String(err)}`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const proc = spawnOnce();
|
|
287
|
+
// Wait for CDP to come up.
|
|
288
|
+
const readyDeadline = Date.now() + 15_000;
|
|
289
|
+
while (Date.now() < readyDeadline) {
|
|
290
|
+
if (await isChromeReachable(profile.cdpUrl, 500)) {
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (!(await isChromeReachable(profile.cdpUrl, 500))) {
|
|
297
|
+
try {
|
|
298
|
+
proc.kill("SIGKILL");
|
|
299
|
+
} catch {
|
|
300
|
+
// ignore
|
|
301
|
+
}
|
|
302
|
+
throw new Error(
|
|
303
|
+
`Failed to start Chrome CDP on port ${profile.cdpPort} for profile "${profile.name}".`,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const pid = proc.pid ?? -1;
|
|
308
|
+
log.info(
|
|
309
|
+
`🦞 openclaw browser started (${exe.kind}) profile "${profile.name}" on 127.0.0.1:${profile.cdpPort} (pid ${pid})`,
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
pid,
|
|
314
|
+
exe,
|
|
315
|
+
userDataDir,
|
|
316
|
+
cdpPort: profile.cdpPort,
|
|
317
|
+
startedAt,
|
|
318
|
+
proc,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export async function stopOpenClawChrome(running: RunningChrome, timeoutMs = 2500) {
|
|
323
|
+
const proc = running.proc;
|
|
324
|
+
if (proc.killed) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
try {
|
|
328
|
+
proc.kill("SIGTERM");
|
|
329
|
+
} catch {
|
|
330
|
+
// ignore
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const start = Date.now();
|
|
334
|
+
while (Date.now() - start < timeoutMs) {
|
|
335
|
+
if (!proc.exitCode && proc.killed) {
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
if (!(await isChromeReachable(cdpUrlForPort(running.cdpPort), 200))) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
proc.kill("SIGKILL");
|
|
346
|
+
} catch {
|
|
347
|
+
// ignore
|
|
348
|
+
}
|
|
349
|
+
}
|