openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
3,173 lines • 120 kB
JavaScript
import { j as resolveIntegerOption } from "./number-coercion-CLj0HTDM.js";
import { r as asNullableRecord } from "./record-coerce-DItp3I4t.js";
import { l as normalizeOptionalString, o as normalizeLowercaseStringOrEmpty } from "./string-coerce-CIXf7egm.js";
import { n as sliceUtf16Safe, r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.js";
import { h as redactToolPayloadText, p as redactSensitiveText } from "./redact-BtvPPfTi.js";
import { t as CONFIG_DIR } from "./utils-P__uGsPB.js";
import { n as resolvePreferredOpenClawTmpDir } from "./tmp-openclaw-dir-DnyL0lW9.js";
import { t as createSubsystemLogger } from "./subsystem-Dy2tqXOS.js";
import { r as isPidAlive } from "./pid-alive-XuW58Ofz.js";
import { s as isLoopbackHost } from "./net-DbNPs6Xm.js";
import { r as ensurePortAvailable } from "./ports-CU06KPCY.js";
import { m as readProviderJsonResponse } from "./provider-http-errors-U-nhuk_f.js";
import { t as normalizeHostname } from "./hostname-_16721Le.js";
import { _ as resolvePinnedHostnameWithPolicy, d as isPrivateNetworkAllowedByPolicy, p as matchesHostnameAllowlist } from "./ssrf-0QyXWOVG.js";
import { t as prepareOomScoreAdjustedSpawn } from "./linux-oom-score-YCP0ajt5.js";
import { t as expectDefined } from "./expect-runtime-CJBt0Gq2.js";
import "./number-runtime-Cy4drVnh.js";
import "./string-coerce-runtime-GQa0ehRA.js";
import { n as rawDataToString } from "./ws-C3ckvj65.js";
import "./process-runtime-pcN9RjT8.js";
import { n as redactCdpUrl } from "./browser-cdp-DfRHhv02.js";
import "./provider-http-k9RMI7iG.js";
import { r as saveJsonFile, t as loadJsonFile } from "./json-store-CByodRuK.js";
import "./text-utility-runtime-BjzvUG99.js";
import "./webhook-ingress-CpRSOilv.js";
import { m as DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME } from "./constants-DKjTvIBP.js";
import { i as resolveManagedBrowserHeadlessMode, t as getManagedBrowserMissingDisplayError } from "./config-BosO4Zbt.js";
import { n as BROWSER_ERROR_REASONS, r as BrowserCdpEndpointBlockedError, s as BrowserProfileUnavailableError } from "./errors-6hPg4OWh.js";
import "./sdk-security-runtime-De1urFYo.js";
import "./tmp-openclaw-dir-rXePUIsE.js";
import { A as assertManagedProxyAllowsCdpUrl, S as CHROME_LAUNCH_READY_WINDOW_MS, T as CHROME_STOP_TIMEOUT_MS, a as fetchJson, b as CHROME_BOOTSTRAP_EXIT_TIMEOUT_MS, c as isWebSocketUrl, f as scopeCdpPolicyToConfiguredEndpoint, h as withCdpSocket, i as fetchCdpChecked, l as normalizeCdpHttpBaseForJsonEndpoints, m as openCdpWebSocket, n as assertCdpEndpointAllowed, s as isDirectCdpWebSocketEndpoint, t as appendCdpPath, w as CHROME_STDERR_HINT_MAX_CHARS, x as CHROME_BOOTSTRAP_PREFS_TIMEOUT_MS } from "./cdp.helpers-CLv_y5y4.js";
import "./subsystem-DbD06g1M.js";
import { t as DEFAULT_DOWNLOAD_DIR } from "./paths-CA_QMzCv.js";
import { t as createBoundedUtf8Tail } from "./bounded-utf8-tail-dlh7Ug_1.js";
import "./ssrf-policy-helpers-CVPn1EtX.js";
import { t as ensureOutputDirectory } from "./output-directories-WMSlqvWx.js";
import fs from "node:fs";
import path from "node:path";
import { execFileSync, spawn } from "node:child_process";
import { isIP } from "node:net";
import os from "node:os";
import { once } from "node:events";
import { setTimeout as setTimeout$1 } from "node:timers/promises";
//#region extensions/browser/src/browser/browser-proxy-mode.ts
const PROXY_ROUTING_CHROME_ARGS = /* @__PURE__ */ new Set([
"--proxy-auto-detect",
"--proxy-pac-url",
"--proxy-server"
]);
const PROXY_CONTROL_CHROME_ARGS = /* @__PURE__ */ new Set(["--no-proxy-server", ...PROXY_ROUTING_CHROME_ARGS]);
const CHROME_PROXY_ENV_KEYS = [
"HTTP_PROXY",
"HTTPS_PROXY",
"ALL_PROXY",
"NO_PROXY",
"http_proxy",
"https_proxy",
"all_proxy",
"no_proxy"
];
function chromeArgName(arg) {
return arg.trim().split("=", 1)[0]?.toLowerCase() ?? "";
}
/** Return true when Chrome args contain any proxy control flag. */
function hasChromeProxyControlArg(args) {
return args.some((arg) => PROXY_CONTROL_CHROME_ARGS.has(chromeArgName(arg)));
}
/** Return true when Chrome args route traffic through an explicit proxy. */
function hasExplicitChromeProxyRoutingArg(args) {
return args.some((arg) => PROXY_ROUTING_CHROME_ARGS.has(chromeArgName(arg)));
}
/** Remove inherited proxy env so launched Chrome follows only configured args. */
function omitChromeProxyEnv(env) {
const next = { ...env };
for (const key of CHROME_PROXY_ENV_KEYS) delete next[key];
return next;
}
/** Resolve the navigation proxy mode used by SSRF/navigation guards. */
function resolveBrowserNavigationProxyMode(params) {
if (params.profile.driver === "openclaw" && params.profile.cdpIsLoopback && !params.profile.attachOnly && hasExplicitChromeProxyRoutingArg(params.resolved.extraArgs)) return "explicit-browser-proxy";
return "direct";
}
//#endregion
//#region extensions/browser/src/browser/cdp-page-session.ts
const CDP_TARGET_NAVIGATION_RESULT_TIMEOUT_MS = 2e3;
const CDP_TARGET_NAVIGATION_RESULT_POLL_MS = 50;
const CDP_TARGET_NAVIGATION_STABILITY_MS = 250;
function readCommittedFrameUrl(frame) {
const unreachableUrl = typeof frame?.unreachableUrl === "string" ? frame.unreachableUrl.trim() : "";
if (unreachableUrl) return unreachableUrl;
const url = typeof frame?.url === "string" ? frame.url.trim() : "";
if (url === ":") return;
const fragment = typeof frame?.urlFragment === "string" ? frame.urlFragment.trim() : "";
return url ? `${url}${fragment}` : void 0;
}
/** Read the browser-owned loader identity for the committed main-frame document. */
async function readCdpMainFrameDocumentIdentity(send, sessionId) {
const loaderId = (await send("Page.getFrameTree", void 0, sessionId).catch(() => null))?.frameTree?.frame?.loaderId;
return typeof loaderId === "string" && loaderId.trim() ? `cdp:${loaderId.trim()}` : void 0;
}
async function waitForCdpNavigationResult(send, sessionId, requestedUrl, signal) {
const deadline = Date.now() + CDP_TARGET_NAVIGATION_RESULT_TIMEOUT_MS;
const requestedAboutBlank = requestedUrl.trim() === "" || requestedUrl.trim() === "about:blank";
let stableCandidate;
while (Date.now() < deadline) {
signal?.throwIfAborted();
const frameTree = await send("Page.getFrameTree", void 0, sessionId).catch(() => null);
signal?.throwIfAborted();
const frame = frameTree?.frameTree?.frame;
const finalUrl = readCommittedFrameUrl(frame);
if (requestedAboutBlank && finalUrl === "about:blank") return finalUrl;
const loaderId = typeof frame?.loaderId === "string" ? frame.loaderId.trim() : "";
if (finalUrl && finalUrl !== "about:blank" && loaderId) {
const key = `${loaderId}\n${finalUrl}`;
const now = Date.now();
if (stableCandidate?.key === key) {
if (now - stableCandidate.since >= CDP_TARGET_NAVIGATION_STABILITY_MS) return finalUrl;
} else stableCandidate = {
key,
since: now
};
} else stableCandidate = void 0;
await new Promise((resolve) => {
setTimeout(resolve, CDP_TARGET_NAVIGATION_RESULT_POLL_MS);
});
}
}
/** Enable the page domains shared by target preparation and page operations. */
async function prepareCdpPageSession(send, sessionId) {
await Promise.all([
send("Page.enable", void 0, sessionId).catch(() => {}),
send("Runtime.enable", void 0, sessionId).catch(() => {}),
send("Network.enable", void 0, sessionId).catch(() => {}),
send("DOM.enable", void 0, sessionId).catch(() => {}),
send("Accessibility.enable", void 0, sessionId).catch(() => {})
]);
await send("Runtime.runIfWaitingForDebugger", void 0, sessionId).catch(() => {});
}
/** Prepare a created target and optionally observe its committed document URL. */
async function prepareCdpTargetSession(send, targetId, navigationUrl, signal) {
const attached = await send("Target.attachToTarget", {
targetId,
flatten: true
}).catch(() => null);
const sessionId = typeof attached?.sessionId === "string" ? attached.sessionId : void 0;
if (!sessionId) return;
try {
await prepareCdpPageSession(send, sessionId);
return navigationUrl === void 0 ? void 0 : await waitForCdpNavigationResult(send, sessionId, navigationUrl, signal);
} finally {
await send("Target.detachFromTarget", { sessionId }).catch(() => {});
}
}
/** Read the committed document URL from a page-level CDP WebSocket. */
async function waitForCdpCommittedNavigationUrl(opts) {
const pinned = await assertCdpEndpointAllowed(opts.wsUrl, opts.cdpPolicy, {
source: "discovered",
configuredUrl: opts.configuredCdpUrl
});
opts.signal?.throwIfAborted();
try {
return await withCdpSocket(opts.wsUrl, async (send) => {
opts.signal?.throwIfAborted();
await send("Page.enable");
return await waitForCdpNavigationResult(send, void 0, opts.requestedUrl, opts.signal);
}, {
commandTimeoutMs: opts.timeouts?.httpTimeoutMs ?? CDP_TARGET_NAVIGATION_RESULT_TIMEOUT_MS,
handshakeTimeoutMs: opts.timeouts?.handshakeTimeoutMs,
handshakeRetries: 0,
lookup: pinned?.lookup
});
} catch {
opts.signal?.throwIfAborted();
return;
}
}
//#endregion
//#region extensions/browser/src/browser/navigation-guard.ts
/**
* Browser navigation SSRF guard.
*
* Validates page navigation URLs and redirect chains before or after browser
* navigation while accounting for browser proxy routing.
*/
const NETWORK_NAVIGATION_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
const SAFE_NON_NETWORK_URLS = /* @__PURE__ */ new Set(["about:blank"]);
const BROWSER_NAVIGATION_CREDENTIALS_BLOCKED_MESSAGE = "Navigation blocked: URL-embedded credentials are not supported for page navigation. Set HTTP Basic auth with `openclaw browser set credentials <username> <password>` or use an authenticated browser profile.";
function isAllowedNonNetworkNavigationUrl(parsed) {
return SAFE_NON_NETWORK_URLS.has(parsed.href);
}
/** Raised when a browser navigation URL fails syntax or policy validation. */
var InvalidBrowserNavigationUrlError = class extends Error {
constructor(message) {
super(message);
this.name = "InvalidBrowserNavigationUrlError";
}
};
/** Parse a page-navigation URL and reject credentials before any transport dispatch. */
function parseBrowserNavigationUrl(url) {
const rawUrl = url.trim();
if (!rawUrl) throw new InvalidBrowserNavigationUrlError("url is required");
let parsed;
try {
parsed = new URL(rawUrl);
} catch {
throw new InvalidBrowserNavigationUrlError(`Invalid URL: ${rawUrl.includes("@") ? "[redacted credential-bearing URL]" : rawUrl}`);
}
if (parsed.username || parsed.password) throw new InvalidBrowserNavigationUrlError(BROWSER_NAVIGATION_CREDENTIALS_BLOCKED_MESSAGE);
return parsed;
}
/** Build a navigation-policy object while omitting default direct proxy mode. */
function withBrowserNavigationPolicy(ssrfPolicy, opts) {
return {
...ssrfPolicy ? { ssrfPolicy } : {},
...opts?.browserProxyMode && opts.browserProxyMode !== "direct" ? { browserProxyMode: opts.browserProxyMode } : {}
};
}
/** Return true when strict policy requires redirect-chain inspection. */
function requiresInspectableBrowserNavigationRedirects(ssrfPolicy) {
return ssrfPolicy?.dangerouslyAllowPrivateNetwork === false;
}
/** Return true when a URL needs redirect inspection under strict policy. */
function requiresInspectableBrowserNavigationRedirectsForUrl(url, ssrfPolicy) {
if (!requiresInspectableBrowserNavigationRedirects(ssrfPolicy)) return false;
try {
const parsed = new URL(url);
return NETWORK_NAVIGATION_PROTOCOLS.has(parsed.protocol);
} catch {
return false;
}
}
function isIpLiteralHostname(hostname) {
return isIP(normalizeHostname(hostname)) !== 0;
}
function isExplicitlyAllowedBrowserHostname(hostname, ssrfPolicy) {
const normalizedHostname = normalizeHostname(hostname);
const allowedHostnames = (ssrfPolicy?.allowedHostnames ?? []).map((pattern) => normalizeHostname(pattern)).filter(Boolean);
return allowedHostnames.length > 0 ? matchesHostnameAllowlist(normalizedHostname, allowedHostnames) : false;
}
/** Assert that a requested browser navigation URL is policy-allowed. */
async function assertBrowserNavigationAllowed(opts) {
const parsed = parseBrowserNavigationUrl(opts.url);
if (!NETWORK_NAVIGATION_PROTOCOLS.has(parsed.protocol)) {
if (isAllowedNonNetworkNavigationUrl(parsed)) return;
throw new InvalidBrowserNavigationUrlError(`Navigation blocked: unsupported protocol "${parsed.protocol}"`);
}
if (opts.browserProxyMode === "explicit-browser-proxy" && !isPrivateNetworkAllowedByPolicy(opts.ssrfPolicy)) throw new InvalidBrowserNavigationUrlError("Navigation blocked: strict browser SSRF policy cannot be enforced while this browser profile is proxy-routed");
if (opts.ssrfPolicy && opts.ssrfPolicy.dangerouslyAllowPrivateNetwork === false && !isPrivateNetworkAllowedByPolicy(opts.ssrfPolicy) && !isIpLiteralHostname(parsed.hostname) && !isExplicitlyAllowedBrowserHostname(parsed.hostname, opts.ssrfPolicy)) throw new InvalidBrowserNavigationUrlError("Navigation blocked: strict browser SSRF policy requires an IP-literal URL because browser DNS rebinding protections are unavailable for hostname-based navigation");
await resolvePinnedHostnameWithPolicy(parsed.hostname, {
lookupFn: opts.lookupFn,
policy: opts.ssrfPolicy
});
}
/**
* Best-effort post-navigation guard for final page URLs.
* Only validates network URLs (http/https) and about:blank to avoid false
* positives on browser-internal error pages (e.g. chrome-error://). In strict
* mode this intentionally re-applies the hostname gate after redirects.
*/
async function assertBrowserNavigationResultAllowed(opts) {
const rawUrl = opts.url.trim();
if (!rawUrl) return;
let parsed;
try {
parsed = new URL(rawUrl);
} catch {
return;
}
if (NETWORK_NAVIGATION_PROTOCOLS.has(parsed.protocol) || isAllowedNonNetworkNavigationUrl(parsed)) await assertBrowserNavigationAllowed(opts);
}
/** Assert that every URL in a browser redirect chain is policy-allowed. */
async function assertBrowserNavigationRedirectChainAllowed(opts) {
const chain = [];
let current = opts.request ?? null;
while (current) {
chain.push(current.url());
current = current.redirectedFrom();
}
for (const url of chain.toReversed()) await assertBrowserNavigationAllowed({
url,
lookupFn: opts.lookupFn,
ssrfPolicy: opts.ssrfPolicy,
browserProxyMode: opts.browserProxyMode
});
}
//#endregion
//#region extensions/browser/src/browser/snapshot-roles.ts
/**
* Shared ARIA role classification sets used by both the Playwright and Chrome MCP
* snapshot paths. Keep these in sync — divergence causes the two drivers to produce
* different snapshot output for the same page.
*/
/** Roles that represent user-interactive elements and always get a ref. */
const INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
"button",
"checkbox",
"combobox",
"link",
"listbox",
"menuitem",
"menuitemcheckbox",
"menuitemradio",
"option",
"radio",
"searchbox",
"slider",
"spinbutton",
"switch",
"tab",
"textbox",
"treeitem"
]);
/** Roles that carry meaningful content and get a ref when named. */
const CONTENT_ROLES = /* @__PURE__ */ new Set([
"article",
"cell",
"columnheader",
"gridcell",
"heading",
"listitem",
"main",
"navigation",
"region",
"rowheader"
]);
/** Structural/container roles — typically skipped in compact mode. */
const STRUCTURAL_ROLES = /* @__PURE__ */ new Set([
"application",
"directory",
"document",
"generic",
"grid",
"group",
"ignored",
"list",
"menu",
"menubar",
"none",
"presentation",
"row",
"rowgroup",
"table",
"tablist",
"toolbar",
"tree",
"treegrid"
]);
//#endregion
//#region extensions/browser/src/browser/pw-role-snapshot.ts
/**
* Playwright role snapshot helpers.
*
* Converts ARIA or AI snapshots into compact role/name text with stable refs
* and duplicate disambiguation for agent actions.
*/
const ROLE_SNAPSHOT_TRUNCATION_MARKER = "[...TRUNCATED - page too large]";
/** Read formatter-owned refs without interpreting names or scalar page content. */
function findRoleSnapshotLineRef(line) {
return parseSnapshotLine(line)?.ref;
}
function getRoleSnapshotIdentityKey(ref, value, mode) {
return mode === "aria" ? ref : `${value.role}\0${value.name ?? ""}\0${value.nth ?? 0}`;
}
/** Build the stable identity set used for per-tab snapshot deltas. */
function getRoleSnapshotIdentityKeys(refs, mode) {
return new Set(Object.entries(refs).map(([ref, value]) => getRoleSnapshotIdentityKey(ref, value, mode)));
}
/** Mark ref-bearing lines that were absent from the previous compatible snapshot. */
function annotateRoleSnapshotDelta(params) {
const markedKeys = /* @__PURE__ */ new Set();
for (const [index, line] of params.lines.entries()) {
const ref = params.lineRefs[index];
const value = ref && Object.hasOwn(params.refs, ref) ? params.refs[ref] : void 0;
if (!ref || !value) continue;
const key = getRoleSnapshotIdentityKey(ref, value, params.mode);
if (params.previousKeys.has(key)) continue;
params.lines[index] = `${line} [new]`;
markedKeys.add(key);
}
if (markedKeys.size === 0) return false;
params.lines.push(`${markedKeys.size} new element(s) since last snapshot`);
return true;
}
function truncateRoleSnapshot(lines, maxChars) {
const marker = maxChars >= 31 ? ROLE_SNAPSHOT_TRUNCATION_MARKER : "…";
let prefix = "";
let lineCount = 0;
for (const line of lines) {
const candidate = prefix ? `${prefix}\n${line}` : line;
if (candidate.length + 2 + marker.length > maxChars) break;
prefix = candidate;
lineCount += 1;
}
return {
snapshot: prefix ? `${prefix}\n\n${marker}` : marker,
lineCount
};
}
/** Apply the final output budget, then keep only refs present on complete output lines. */
function finalizeRoleSnapshot(params) {
const normalizedMaxChars = typeof params.maxChars === "number" && Number.isFinite(params.maxChars) && params.maxChars > 0 ? Math.floor(params.maxChars) : void 0;
const maxChars = normalizedMaxChars && normalizedMaxChars > 0 ? normalizedMaxChars : void 0;
const delta = params.delta;
const previousKeys = delta?.previousKeys;
const sourceLines = params.snapshot.split("\n");
let lineRefs;
let annotated = false;
if (delta && previousKeys !== void 0) {
lineRefs = sourceLines.map(findRoleSnapshotLineRef);
annotated = annotateRoleSnapshotDelta({
lines: sourceLines,
lineRefs,
refs: params.refs,
mode: delta.mode,
previousKeys
});
}
const sourceSnapshot = annotated ? sourceLines.join("\n") : params.snapshot;
const truncated = maxChars !== void 0 && sourceSnapshot.length > maxChars;
const bounded = truncated ? truncateRoleSnapshot(sourceLines, maxChars) : void 0;
const snapshot = bounded?.snapshot ?? sourceSnapshot;
const outputLines = truncated ? snapshot.split("\n") : sourceLines;
const visibleRefs = /* @__PURE__ */ new Set();
const visibleLineCount = bounded?.lineCount ?? sourceLines.length;
for (let index = 0; index < visibleLineCount; index += 1) {
const ref = lineRefs ? lineRefs[index] : findRoleSnapshotLineRef(sourceLines[index]);
if (ref) visibleRefs.add(ref);
}
const visibleEntries = [];
const newKeys = previousKeys !== void 0 ? /* @__PURE__ */ new Set() : void 0;
let interactive = 0;
for (const [ref, value] of Object.entries(params.refs)) {
if (!visibleRefs.has(ref)) continue;
visibleEntries.push([ref, value]);
if (INTERACTIVE_ROLES.has(value.role)) interactive += 1;
if (newKeys && delta && previousKeys !== void 0) {
const key = getRoleSnapshotIdentityKey(ref, value, delta.mode);
if (!previousKeys.has(key)) newKeys.add(key);
}
}
const refs = Object.fromEntries(visibleEntries);
const newElements = newKeys?.size;
const result = {
snapshot,
refs,
stats: {
lines: snapshot ? outputLines.length : 0,
chars: snapshot.length,
refs: visibleEntries.length,
interactive
},
...newElements !== void 0 ? { newElements } : {}
};
return truncated ? {
...result,
truncated: true
} : result;
}
function getIndentLevel(line) {
const indent = line.match(/^(\s*)/)?.[1];
return indent === void 0 ? 0 : Math.floor(indent.length / 2);
}
function parseSnapshotLine(line) {
const entry = line.match(/^(\s*-\s+)(.*)$/s);
if (!entry) return null;
const prefix = entry[1];
const content = entry[2];
const quoted = content.match(/^'((?:[^']|'')*)'(.*)$/s);
const match = (quoted ? quoted[1].replaceAll("''", "'") : content).match(/^(\w+)(?:\s+("(?:\\.|[^"\\])*"))?(.*)$/s);
if (!match) return null;
const roleRaw = match[1];
let nameToken = match[2];
let suffix = match[3];
if (nameToken === void 0 && suffix.startsWith(" /")) {
const literal = (quoted ? suffix : suffix.split(/:(?=\s|$)/, 1)[0]).match(/^ (\/(?:.*\/)?)/s);
if (literal) {
nameToken = literal[1];
suffix = suffix.slice(literal[0].length);
}
}
const ref = (suffix.match(/^(?:\s+\[[^\][]*\])*/)?.[0])?.match(/\[ref=([^\][]+)\]/)?.[1];
return {
prefix,
roleRaw,
role: normalizeLowercaseStringOrEmpty(roleRaw),
nameToken,
ref,
suffix: suffix + (quoted?.[2] ?? "")
};
}
function decodeSnapshotName(nameToken) {
return nameToken?.startsWith("\"") ? JSON.parse(nameToken) : nameToken;
}
function createRoleNameTracker() {
const groups = /* @__PURE__ */ new Map();
return (role, name) => {
const key = `${role}:${name ?? ""}`;
const group = groups.get(key);
const data = {
role,
name
};
if (group) {
if (group.count === 1) group.first.nth = 0;
data.nth = group.count;
group.count += 1;
} else groups.set(key, {
count: 1,
first: data
});
return data;
};
}
function compactTree(tree) {
const lines = tree.split("\n");
const entries = [];
const stack = [];
const finishEntry = () => {
const current = stack.pop();
if (!current) return;
current.entry.keep ||= current.entry.hasRef;
if (current.entry.hasRef && stack.length > 0) {
const parent = stack.at(-1);
if (parent !== void 0) parent.entry.hasRef = true;
}
};
for (const line of lines) {
const indent = getIndentLevel(line);
while (stack.length > 0) {
if (expectDefined(stack.at(-1), "non-empty role snapshot stack").indent < indent) break;
finishEntry();
}
const hasRef = Boolean(findRoleSnapshotLineRef(line));
const entry = {
line,
keep: hasRef || line.includes(":") && !line.trimEnd().endsWith(":"),
hasRef,
indent
};
entries.push(entry);
stack.push({
entry,
indent
});
}
while (stack.length > 0) finishEntry();
return entries.filter((entry) => entry.keep).map((entry) => entry.line).join("\n") || "(empty)";
}
function processLine(line, refs, options, tracker, nextRef) {
const depth = getIndentLevel(line);
if (options.maxDepth !== void 0 && depth > options.maxDepth) return null;
const parsed = parseSnapshotLine(line);
if (!parsed) return options.interactive ? null : line;
const { prefix, roleRaw, role, suffix } = parsed;
const name = decodeSnapshotName(parsed.nameToken);
const isInteractive = INTERACTIVE_ROLES.has(role);
const isContent = CONTENT_ROLES.has(role);
const isStructural = STRUCTURAL_ROLES.has(role);
if (options.interactive && !isInteractive) return null;
if (options.compact && isStructural && !name) return null;
if (!(isInteractive || isContent && name)) return line;
const ref = nextRef();
const data = tracker(role, name);
refs[ref] = data;
const nth = data.nth ?? 0;
let enhanced = `${prefix}${roleRaw}`;
if (name) enhanced += ` ${JSON.stringify(name)}`;
enhanced += ` [ref=${ref}]`;
if (nth > 0) enhanced += ` [nth=${nth}]`;
if (suffix) enhanced += suffix;
return enhanced;
}
function buildInteractiveSnapshotLines(params) {
const out = [];
for (const line of params.lines) {
if (params.options.maxDepth !== void 0 && getIndentLevel(line) > params.options.maxDepth) continue;
const entry = parseSnapshotLine(line);
if (!entry) continue;
const parsed = {
...entry,
name: decodeSnapshotName(entry.nameToken)
};
if (!INTERACTIVE_ROLES.has(parsed.role)) continue;
const resolved = params.resolveRef(parsed);
if (!resolved?.ref) continue;
params.refs[resolved.ref] = resolved.data;
let enhanced = `- ${parsed.roleRaw}`;
if (parsed.name) enhanced += ` ${JSON.stringify(parsed.name)}`;
enhanced += ` [ref=${resolved.ref}]`;
if ((resolved.data.nth ?? 0) > 0) enhanced += ` [nth=${resolved.data.nth}]`;
enhanced += params.formatSuffix(parsed.suffix, resolved.ref);
out.push(enhanced);
}
return out;
}
/** Normalize a role snapshot ref accepted by browser actions. */
function parseRoleRef(raw) {
const trimmed = raw.trim();
if (!trimmed) return null;
const normalized = trimmed.startsWith("@") ? trimmed.slice(1) : trimmed.startsWith("ref=") ? trimmed.slice(4) : trimmed;
if (/^e\d+$/i.test(normalized)) return normalized;
if (/^\d{1,9}$/.test(normalized)) return normalized;
return null;
}
/** Build a role snapshot and refs from Playwright ARIA snapshot text. */
function buildRoleSnapshotFromAriaSnapshot(ariaSnapshot, options = {}) {
const lines = ariaSnapshot.split("\n");
const refs = {};
const tracker = createRoleNameTracker();
let counter = 0;
const nextRef = () => {
counter += 1;
return `e${counter}`;
};
if (options.interactive) return {
snapshot: buildInteractiveSnapshotLines({
lines,
options,
refs,
resolveRef: ({ role, name }) => ({
ref: nextRef(),
data: tracker(role, name)
}),
formatSuffix: (suffix) => suffix.includes("[") ? suffix : ""
}).join("\n") || "(no interactive elements)",
refs
};
const result = [];
for (const line of lines) {
const processed = processLine(line, refs, options, tracker, nextRef);
if (processed !== null) result.push(processed);
}
const tree = result.join("\n") || "(empty)";
return {
snapshot: options.compact ? compactTree(tree) : tree,
refs
};
}
function parseAiSnapshotRef(ref) {
return ref && /^(?:f\d+)?e\d+$|^\d{1,9}$/i.test(ref) ? ref : null;
}
/**
* Build a role snapshot from Playwright's AI snapshot output while preserving Playwright's own
* aria-ref ids (e.g. ref=e13). This makes the refs self-resolving across calls.
*/
/** Build a role snapshot and refs from Playwright AI snapshot text. */
function buildRoleSnapshotFromAiSnapshot(aiSnapshot, options = {}) {
const lines = aiSnapshot.split("\n");
const refs = {};
if (options.interactive) return {
snapshot: buildInteractiveSnapshotLines({
lines,
options,
refs,
resolveRef: (parsed) => {
const ref = parseAiSnapshotRef(parsed.ref);
return ref ? {
ref,
data: {
role: parsed.role,
...parsed.name ? { name: parsed.name } : {}
}
} : null;
},
formatSuffix: (suffix, ref) => suffix.replace(` [ref=${ref}]`, "")
}).join("\n") || "(no interactive elements)",
refs
};
const out = [];
for (const line of lines) {
const depth = getIndentLevel(line);
if (options.maxDepth !== void 0 && depth > options.maxDepth) continue;
const parsed = parseSnapshotLine(line);
if (!parsed) {
out.push(line);
continue;
}
const { role } = parsed;
const name = decodeSnapshotName(parsed.nameToken);
const isStructural = STRUCTURAL_ROLES.has(role);
if (options.compact && isStructural && !name) continue;
const ref = parseAiSnapshotRef(parsed.ref);
if (ref) refs[ref] = {
role,
...name ? { name } : {}
};
out.push(line);
}
const tree = out.join("\n") || "(empty)";
return {
snapshot: options.compact ? compactTree(tree) : tree,
refs
};
}
//#endregion
//#region extensions/browser/src/browser/snapshot-depth-limit.ts
const ROLE_SNAPSHOT_DEPTH_TRUNCATION_MARKER = "[...TRUNCATED - accessibility tree too deep]";
function appendRoleSnapshotDepthTruncationMarker(snapshot) {
return snapshot ? `${snapshot}\n\n${ROLE_SNAPSHOT_DEPTH_TRUNCATION_MARKER}` : ROLE_SNAPSHOT_DEPTH_TRUNCATION_MARKER;
}
//#endregion
//#region extensions/browser/src/browser/cdp.ts
/**
* Chrome DevTools Protocol browser operations.
*
* Provides screenshots, target creation, JavaScript evaluation, ARIA/role
* snapshots, DOM text, and selector lookup on top of the CDP socket helpers.
*/
/** Read the current main-frame loader identity from a page-level CDP target. */
async function getMainFrameDocumentIdentityViaCdp(opts) {
return await withCdpSocket(opts.wsUrl, async (send) => await readCdpMainFrameDocumentIdentity(send), {
commandTimeoutMs: opts.timeoutMs ?? 5e3,
...opts.lookup ? { lookup: opts.lookup } : {}
});
}
/** Normalize a reported CDP WebSocket URL against the configured CDP base URL. */
function normalizeCdpWsUrl(wsUrl, cdpUrl) {
const ws = new URL(wsUrl);
const cdp = new URL(cdpUrl);
const isWildcardBind = ws.hostname === "0.0.0.0" || ws.hostname === "[::]";
if ((isLoopbackHost(ws.hostname) || isWildcardBind) && !isLoopbackHost(cdp.hostname)) {
ws.hostname = cdp.hostname;
const cdpPort = cdp.port || (cdp.protocol === "https:" ? "443" : "80");
/* c8 ignore next 3 */
if (cdpPort) ws.port = cdpPort;
ws.protocol = cdp.protocol === "https:" ? "wss:" : "ws:";
} else if (isLoopbackHost(ws.hostname) && isLoopbackHost(cdp.hostname)) {
ws.hostname = cdp.hostname;
if (!ws.port && cdp.port) ws.port = cdp.port;
}
if (cdp.protocol === "https:" && ws.protocol === "ws:") ws.protocol = "wss:";
if (!ws.username && !ws.password && (cdp.username || cdp.password)) {
ws.username = cdp.username;
ws.password = cdp.password;
}
for (const [key, value] of cdp.searchParams.entries()) if (!ws.searchParams.has(key)) ws.searchParams.append(key, value);
return ws.toString();
}
/** Capture a PNG or JPEG screenshot through CDP, optionally full-page. */
async function captureScreenshot(opts) {
return await withCdpSocket(opts.wsUrl, async (send) => {
await send("Page.enable");
if (opts.headless !== false) await send("Page.bringToFront").catch(() => {});
const format = opts.format ?? "png";
const quality = format === "jpeg" ? Math.max(0, Math.min(100, Math.round(opts.quality ?? 85))) : void 0;
const base64 = (await send("Page.captureScreenshot", {
format,
...quality !== void 0 ? { quality } : {},
...opts.fullPage ? { captureBeyondViewport: true } : {}
}))?.data;
if (!base64) throw new Error("Screenshot failed: missing data");
return Buffer.from(base64, "base64");
}, {
commandTimeoutMs: opts.timeoutMs,
lookup: opts.lookup
});
}
/** Create a new browser target after applying navigation and CDP SSRF policy. */
async function createTargetViaCdp(opts) {
opts.signal?.throwIfAborted();
await assertBrowserNavigationAllowed({
url: opts.url,
...withBrowserNavigationPolicy(opts.ssrfPolicy)
});
const configuredCdpPin = await assertCdpEndpointAllowed(opts.cdpUrl, opts.ssrfPolicy);
const cdpControlPolicy = scopeCdpPolicyToConfiguredEndpoint(opts.cdpUrl, opts.ssrfPolicy);
let wsUrl;
if (isDirectCdpWebSocketEndpoint(opts.cdpUrl)) wsUrl = opts.cdpUrl;
else {
const discoveryUrl = isWebSocketUrl(opts.cdpUrl) ? normalizeCdpHttpBaseForJsonEndpoints(opts.cdpUrl) : opts.cdpUrl;
let version = null;
try {
version = await fetchJson(appendCdpPath(discoveryUrl, "/json/version"), opts.timeouts?.httpTimeoutMs, { signal: opts.signal }, cdpControlPolicy);
} catch (err) {
if (!isWebSocketUrl(opts.cdpUrl)) throw err;
}
const wsUrlRaw = version?.webSocketDebuggerUrl?.trim() ?? "";
if (wsUrlRaw) wsUrl = normalizeCdpWsUrl(wsUrlRaw, discoveryUrl);
else if (isWebSocketUrl(opts.cdpUrl)) wsUrl = opts.cdpUrl;
else throw new Error("CDP /json/version missing webSocketDebuggerUrl");
}
const candidateWsUrls = isWebSocketUrl(opts.cdpUrl) && wsUrl !== opts.cdpUrl ? [wsUrl, opts.cdpUrl] : [wsUrl];
let lastError;
for (const candidateWsUrl of candidateWsUrls) try {
const endpointSource = candidateWsUrl === opts.cdpUrl ? { source: "configured" } : {
source: "discovered",
configuredUrl: opts.cdpUrl
};
const candidateCdpPin = candidateWsUrl === opts.cdpUrl ? configuredCdpPin : await assertCdpEndpointAllowed(candidateWsUrl, cdpControlPolicy, endpointSource);
opts.signal?.throwIfAborted();
return await withCdpSocket(candidateWsUrl, async (send) => {
opts.signal?.throwIfAborted();
const targetId = (await send("Target.createTarget", {
url: opts.url,
background: true
}))?.targetId?.trim() ?? "";
if (!targetId) throw new Error("CDP Target.createTarget returned no targetId");
try {
opts.signal?.throwIfAborted();
const finalUrl = await prepareCdpTargetSession(send, targetId, opts.waitForNavigationResult ? opts.url : void 0, opts.signal);
opts.signal?.throwIfAborted();
return finalUrl ? {
targetId,
finalUrl
} : { targetId };
} catch (error) {
await send("Target.closeTarget", { targetId }).catch(() => {});
throw error;
}
}, {
commandTimeoutMs: opts.timeouts?.httpTimeoutMs ?? 5e3,
handshakeTimeoutMs: opts.timeouts?.handshakeTimeoutMs,
lookup: candidateCdpPin?.lookup
});
} catch (err) {
opts.signal?.throwIfAborted();
lastError = err;
}
if (lastError instanceof Error) throw lastError;
throw new Error("CDP Target.createTarget failed");
}
/** Prefix assigned to generated accessibility-node refs. */
const AX_REF_PREFIX = "ax";
const AX_REF_PATTERN = new RegExp(`^${AX_REF_PREFIX}\\d+$`);
function axValue(v) {
if (!v || typeof v !== "object") return "";
const value = v.value;
if (typeof value === "string") return value;
if (typeof value === "number" || typeof value === "boolean") return String(value);
return "";
}
/** Format raw AX nodes into bounded ARIA snapshot nodes. */
function formatAriaSnapshot(nodes, limit) {
const byId = /* @__PURE__ */ new Map();
for (const n of nodes) if (n.nodeId) byId.set(n.nodeId, n);
const referenced = /* @__PURE__ */ new Set();
for (const n of nodes) for (const c of n.childIds ?? []) referenced.add(c);
const root = nodes.find((n) => n.nodeId && !referenced.has(n.nodeId)) ?? nodes[0];
if (!root?.nodeId) return [];
const out = [];
const stack = [{
id: root.nodeId,
depth: 0
}];
while (stack.length && out.length < limit) {
const popped = stack.pop();
/* c8 ignore next 3 */
if (!popped) break;
const { id, depth } = popped;
const n = byId.get(id);
/* c8 ignore next 3 */
if (!n) continue;
const role = axValue(n.role);
const name = axValue(n.name);
const value = axValue(n.value);
const description = axValue(n.description);
const ref = `${AX_REF_PREFIX}${out.length + 1}`;
out.push({
ref,
role: role || "unknown",
name: name || "",
...value ? { value } : {},
...description ? { description } : {},
...typeof n.backendDOMNodeId === "number" ? { backendDOMNodeId: n.backendDOMNodeId } : {},
depth
});
const children = n.childIds ?? [];
for (let i = children.length - 1; i >= 0; i--) {
const child = children[i];
if (child && byId.has(child)) stack.push({
id: child,
depth: depth + 1
});
}
}
return out;
}
/** Capture an accessibility-tree snapshot through CDP. */
async function snapshotAria(opts) {
const limit = resolveIntegerOption(opts.limit, 500, {
min: 1,
max: 2e3
});
return await withCdpSocket(opts.wsUrl, async (send) => {
await prepareCdpPageSession(send);
const res = await send("Accessibility.getFullAXTree");
return { nodes: formatAriaSnapshot(Array.isArray(res?.nodes) ? res.nodes : [], limit) };
}, {
commandTimeoutMs: opts.timeoutMs ?? 5e3,
lookup: opts.lookup
});
}
function buildRoleTree(nodes) {
const byId = /* @__PURE__ */ new Map();
const tree = [];
for (const raw of nodes) {
const nodeId = raw.nodeId ?? "";
if (!nodeId) continue;
byId.set(nodeId, tree.length);
tree.push({
raw,
role: axValue(raw.role) || "unknown",
name: axValue(raw.name),
value: axValue(raw.value),
backendDOMNodeId: typeof raw.backendDOMNodeId === "number" && raw.backendDOMNodeId > 0 ? Math.floor(raw.backendDOMNodeId) : void 0,
children: [],
depth: 0
});
}
const childIndexes = /* @__PURE__ */ new Set();
for (let index = 0; index < tree.length; index += 1) for (const childId of tree[index]?.raw.childIds ?? []) {
const childIndex = byId.get(childId);
if (childIndex === void 0) continue;
tree[index]?.children.push(childIndex);
expectDefined(tree[childIndex], "CDP child node index").parent = index;
childIndexes.add(childIndex);
}
const roots = tree.map((_node, index) => index).filter((index) => !childIndexes.has(index));
const stack = roots.map((index) => ({
index,
depth: 0
}));
while (stack.length) {
const current = stack.pop();
if (!current) break;
const node = expectDefined(tree[current.index], "CDP traversal node index");
node.depth = current.depth;
for (let i = node.children.length - 1; i >= 0; i--) {
const child = expectDefined(node.children[i], "CDP traversal child index");
stack.push({
index: child,
depth: current.depth + 1
});
}
}
return {
tree,
roots: roots.length ? roots : tree.length ? [0] : []
};
}
function shouldIncludeRoleNode(node, options) {
const role = node.role.toLowerCase();
if (options.interactive) return INTERACTIVE_ROLES.has(role) || role === "iframe" || Boolean(node.cursorInfo);
if (options.compact && STRUCTURAL_ROLES.has(role) && !node.name && !node.ref) return false;
return true;
}
function cursorSuffix(info) {
if (!info) return "";
const parts = [
info.hasCursorPointer ? "cursor:pointer" : void 0,
info.hasOnClick ? "onclick" : void 0,
info.hasTabIndex ? "tabindex" : void 0,
info.isEditable ? "contenteditable" : void 0,
info.hiddenInputType ? `hidden-${info.hiddenInputType}` : void 0
].filter(Boolean);
return parts.length ? ` [${parts.join(", ")}]` : "";
}
function renderRoleTree(tree, index, output, options, state, indentOffset = 0) {
const node = tree[index];
if (!node) return;
if (options.maxDepth !== void 0 && node.depth > options.maxDepth) return;
const effectiveDepth = Math.max(0, node.depth + indentOffset);
if (effectiveDepth > 100) {
state.truncated = true;
return;
}
if (shouldIncludeRoleNode(node, options)) {
const indent = " ".repeat(effectiveDepth);
const name = node.name ? ` ${JSON.stringify(node.name)}` : "";
const ref = node.ref ? ` [ref=${node.ref}]` : "";
const nth = node.nth !== void 0 && node.nth > 0 ? ` [nth=${node.nth}]` : "";
const value = node.value ? ` value=${JSON.stringify(node.value)}` : "";
const url = node.url ? ` [url=${node.url}]` : "";
output.push(`${indent}- ${node.role}${name}${ref}${nth}${value}${url}${cursorSuffix(node.cursorInfo)}`);
}
for (const child of node.children) renderRoleTree(tree, child, output, options, state, indentOffset);
}
async function findCursorInteractiveElements(send, sessionId) {
const attr = "data-openclaw-cdp-ci";
const evaluated = await send("Runtime.evaluate", {
expression: `(() => {
const out = [];
const roles = new Set(["button","link","textbox","checkbox","radio","combobox","listbox","menuitem","menuitemcheckbox","menuitemradio","option","searchbox","slider","spinbutton","switch","tab","treeitem"]);
const tags = new Set(["a","button","input","select","textarea","details","summary"]);
document.querySelectorAll("[${attr}]").forEach((el) => el.removeAttribute("${attr}"));
for (const el of Array.from(document.body ? document.body.querySelectorAll("*") : [])) {
if (!(el instanceof HTMLElement) || el.closest("[hidden],[aria-hidden='true']")) continue;
const tagName = el.tagName.toLowerCase();
if (tags.has(tagName)) continue;
const role = String(el.getAttribute("role") || "").toLowerCase();
if (roles.has(role)) continue;
const style = getComputedStyle(el);
const hasCursorPointer = style.cursor === "pointer";
const hasOnClick = el.hasAttribute("onclick") || el.onclick !== null;
const tabIndex = el.getAttribute("tabindex");
const hasTabIndex = tabIndex !== null && tabIndex !== "-1";
const ce = el.getAttribute("contenteditable");
const isEditable = ce === "" || ce === "true";
if (!hasCursorPointer && !hasOnClick && !hasTabIndex && !isEditable) continue;
if (hasCursorPointer && !hasOnClick && !hasTabIndex && !isEditable) {
const parent = el.parentElement;
if (parent && getComputedStyle(parent).cursor === "pointer") continue;
}
const rect = el.getBoundingClientRect();
if (rect.width <= 0 || rect.height <= 0) continue;
let hiddenInputType = "";
const hiddenInput = el.querySelector("input[type='radio'],input[type='checkbox']");
if (hiddenInput instanceof HTMLInputElement) {
const hiddenStyle = getComputedStyle(hiddenInput);
if (hiddenInput.hidden || hiddenStyle.display === "none" || hiddenStyle.visibility === "hidden") {
hiddenInputType = hiddenInput.type;
}
}
el.setAttribute("${attr}", String(out.length));
out.push({
text: String(el.textContent || "").replace(/\\s+/g, " ").trim().slice(0, 101),
tagName,
hasCursorPointer,
hasOnClick,
hasTabIndex,
isEditable,
hiddenInputType,
});
}
return out;
})()`,
returnByValue: true,
awaitPromise: false
}, sessionId).catch(() => null);
const entries = Array.isArray(evaluated?.result?.value) ? evaluated.result.value.map((entry) => {
entry.text = truncateUtf16Safe(entry.text, 100);
return entry;
}) : [];
if (!entries.length) return /* @__PURE__ */ new Map();
const rootNodeId = (await send("DOM.getDocument", { depth: 0 }, sessionId).catch(() => null))?.root?.nodeId;
if (typeof rootNodeId !== "number") return /* @__PURE__ */ new Map();
const queried = await send("DOM.querySelectorAll", {
nodeId: rootNodeId,
selector: `[${attr}]`
}, sessionId).catch(() => null);
const out = /* @__PURE__ */ new Map();
await Promise.all((queried?.nodeIds ?? []).map(async (nodeId) => {
const described = await send("DOM.describeNode", { nodeId }, sessionId).catch(() => null);
const attrs = described?.node?.attributes ?? [];
const attrIndex = attrs.indexOf(attr);
const rawIndex = attrIndex >= 0 ? attrs[attrIndex + 1] : void 0;
const index = typeof rawIndex === "string" ? Number(rawIndex) : NaN;
const backendNodeId = described?.node?.backendNodeId;
if (typeof backendNodeId === "number" && Number.isInteger(index) && entries[index]) out.set(backendNodeId, entries[index]);
}));
await send("Runtime.evaluate", {
expression: `document.querySelectorAll("[${attr}]").forEach((el) => el.removeAttribute("${attr}"))`,
returnByValue: true
}, sessionId).catch(() => {});
return out;
}
async function resolveLinkUrls(send, refs, sessionId) {
const out = /* @__PURE__ */ new Map();
const linkRefs = Object.values(refs).filter((ref) => ref.role === "link" && Boolean(ref.backendDOMNodeId));
await Promise.all(linkRefs.map(async (ref) => {
const objectId = (await send("DOM.resolveNode", { backendNodeId: ref.backendDOMNodeId }, sessionId).catch(() => null))?.object?.objectId;
if (!objectId) return;
const hrefResult = await send("Runtime.callFunctionOn", {
objectId,
functionDeclaration: "function() { return this.href || ''; }",
returnByValue: true
}, sessionId).catch(() => null);
const href = typeof hrefResult?.result?.value === "string" ? hrefResult.result.value : "";
if (href) out.set(ref.backendDOMNodeId, href);
}));
return out;
}
async function resolveIframeFrameIds(send, tree, sessionId) {
const out = /* @__PURE__ */ new Map();
const iframeNodes = tree.filter((node) => node.role.toLowerCase() === "iframe" && Boolean(node.backendDOMNodeId));
await Promise.all(iframeNodes.map(async (node) => {
const described = await send("DOM.describeNode", {
backendNodeId: node.backendDOMNodeId,
depth: 1
}, sessionId).catch(() => null);
const frameId = described?.node?.contentDocument?.frameId ?? described?.node?.frameId ?? "";
if (frameId) out.set(node.backendDOMNodeId, frameId);
}));
return out;
}
async function buildCdpRoleSnapshot(params) {
const res = await params.send("Accessibility.getFullAXTree", params.frameId ? { frameId: params.frameId } : void 0, params.sessionId);
const { tree, roots } = buildRoleTree(Array.isArray(res.nodes) ? res.nodes : []);
const cursorElements = await findCursorInteractiveElements(params.send, params.sessionId);
for (const node of tree) if (node.backendDOMNodeId && cursorElements.has(node.backendDOMNodeId)) {
const cursorInfo = cursorElements.get(node.backendDOMNodeId);
node.cursorInfo = cursorInfo;
if (!node.name && cursorInfo?.text) node.name = cursorInfo.text;
}
const counts = /* @__PURE__ */ new Map();
const refs = {};
for (const node of tree) {
const role = node.role.toLowerCase();
if (!(INTERACTIVE_ROLES.has(role) || CONTENT_ROLES.has(role) && Boolean(node.name) || role === "iframe" || Boolean(node.cursorInfo))) continue;
const key = `${role}:${node.name}`;
const nth = counts.get(key) ?? 0;
counts.set(key, nth + 1);
const ref = `e${params.nextRef.value}`;
params.nextRef.value += 1;
node.ref = ref;
node.nth = nth;
refs[ref] = {
role,
...node.name ? { name: node.name } : {},
nth,
...node.backendDOMNodeId ? { backendDOMNodeId: node.backendDOMNodeId } : {},
...params.frameId ? { frameId: params.frameId } : {}
};
}
for (const node of tree) if (node.ref && counts.get(`${node.role.toLowerCase()}:${node.name}`) === 1) delete refs[node.ref]?.nth;
const iframeFrameIds = await resolveIframeFrameIds(params.send, tree, params.sessionId);
for (const node of tree) if (node.backendDOMNodeId && iframeFrameIds.has(node.backendDOMNodeId)) {
node.frameId = iframeFrameIds.get(node.backendDOMNodeId);
if (node.ref && refs[node.ref]) expectDefined(refs[node.ref], "owned CDP role reference").frameId = node.frameId;
}
if (params.urls) {
const urls = await resolveLinkUrls(params.send, refs, params.sessionId);
for (const node of tree) if (node.backendDOMNodeId && urls.has(node.backendDOMNodeId)) node.url = urls.get(node.backendDOMNodeId);
}
const lines = [];
const renderState = { truncated: false };
for (const root of roots) renderRoleTree(tree, root, lines, params.options, renderState);
if (params.recurseIframes) {
const iframeNodes = tree.filter((node) => node.ref && node.frameId);
for (const iframe of iframeNodes) {
const lineIndex = lines.findIndex((line) => findRoleSnapshotLineRef(line) === iframe.ref);
if (lineIndex < 0 || !iframe.frameId) continue;
const child = await buildCdpRoleSnapshot({
...params,
frameId: iframe.frameId,
recurseIframes: false
}).catch(() => null);
if (!child) continue;
renderState.truncated ||= child.truncated;
if (!child.lines.length) continue;
Object.assign(refs, child.refs);
lines.splice(lineIndex + 1, 0, ...child.lines.map((line) => ` ${line}`));
}
}
return {
lines,
refs,
truncated: renderState.truncated
};
}
/** Build a role/name text snapshot with stable refs from CDP DOM and AX data. */
async function snapshotRoleViaCdp(opts) {
return await withCdpSocket(opts.wsUrl, async (send) => {
await prepareCdpPageSession(send);
const built = await buildCdpRoleSnapshot({
send,
options: opts.options ?? {},
urls: opts.urls,
recurseIframes: opts.recurseIframes ?? true,
nextRef: { value: 1 }
});
const renderedSnapshot = built.lines.join("\n").trim() || (opts.options?.interactive ? "(no interactive elements)" : "(empty page)");
const finalized = finalizeRoleSnapshot({
snapshot: built.truncated ? appendRoleSnapshotDepthTruncationMarker(renderedSnapshot) : renderedSnapshot,
refs: built.refs,
maxChars: opts.maxChars,
delta: opts.delta
});
return built.truncated && !finalized.truncated ? {
...finalized,
truncated: true
} : finalized;
}, {
commandTimeoutMs: opts.timeoutMs ?? 5e3,
lookup: opts.lookup
});
}
//#endregion
//#region extensions/browser/src/browser/chrome.diagnostics.ts
/**
* Chrome CDP diagnostics.
*
* Probes /json/version and WebSocket health, redacts sensitive endpoint data,
* and formats status output for browser doctor/status flows.
*/
function elapsedSince(startedAt) {
return Math.max(0, Date.now() - startedAt);
}
/** Convert an error and optional cause to redacted diagnostic text. */
function safeChromeCdpErrorMessage(error) {
const message = error instanceof Error ? error.message : String(error);
const cause = error instanceof Error ? error.cause : void 0;
const causeMessage = cause instanceof Error ? cause.message : typeof cause === "string" ? cause : void 0;
if (message && causeMessage && !message.includes(causeMessage)) return redactSensitiveText(`${message}: ${causeMessage}`);
return redactSensitiveText(message || "unknown error");
}
function failureDiagnostic(params) {
return {
ok: false,
cdpUrl: params.cdpUrl,
wsUrl: params.wsUrl,
code: params.code,
message: redactSensitiveText(params.message),
elapsedMs: elapsedSince(params.startedAt)
};
}
/** Read and validate Chrome's /json/version endpoint. */
async function readChromeVersion(cdpUrl, timeoutMs = 500, ssrfPolicy, versionPath = "/json/version") {
const ctrl = new AbortController();
const t = setTimeout(ctrl.abort.bind(ctrl), timeoutMs);
try {
const versionUrl = appendCdpPath(cdpUrl, versionPath);
const { response, release } = await fetchCdpChecked(versionUrl, timeoutMs, { signal: ctrl.signal }, ssrfPolicy);
try {
const data = await readProviderJsonResponse(response, "cdp-version");
if (!data || typeof data !== "object") throw new Error("CDP /json/version returned non-object JSON");
return data;
} finally {
await release();
}
} finally {
clearTimeout(t);
}
}
/** Preserve providers that expose only Playwright's trailing-slash route. */
async function readChromeVersionWithCredentialFallback(cdpUrl, timeoutMs = 500, ssrfPolicy) {
try {
const primaryVersion = await readChromeVersion(cdpUrl, timeoutMs, ssrfPolicy);
if (normalizeOptionalString(primaryVersion.webSocketDebuggerUrl)) return primaryVersion;
try {
return await readChromeVersion(cdpUrl, timeoutMs, ssrfPolicy, "/json/version/");
} catch {
return primaryVersion;
}
} catch (primaryError) {
try {
return await readChromeVersion(cdpUrl, timeoutMs, ssrfPolicy, "/json/version/");
} catch {
throw primaryError;
}
}
}
function readObjectString(value, key) {
if (!value || typeof value !== "object") return;
return normalizeOptionalString(value[key]);
}
function chromeVersionFromCdpResult(result) {
const browser = readObjectString(result, "Browser") ?? readObjectString(result, "product");
const userAgent = readObjectString(result, "User-Agent") ?? readObjectString(result, "userAgent");
if (!browser && !userAgent) return;
return {
Browser: browser,
"User-Agent": userAgent
};
}
async function diagnoseCdpHealthCommand(wsUrl, timeoutMs = 800, lookup) {
return await new Promise((resolve) => {
const ws = openCdpWebSocket(wsUrl, {
handshakeTimeoutMs: timeoutMs,
lookup
});
let settled = false;
let opened = false;
const onMessage = (raw) => {
if (settled) return;
let parsed;
try {
parsed = JSON.parse(rawDataToString(raw));
} catch {
return;
}
if (parsed?.id !== 1) return;
if (parsed.result && typeof parsed.result === "object") {
finish({
ok: true,
version: chromeVersionFromCdpResult(parsed.result)
});
return;
}
finish({
ok: false,
code: "websocket_health_command_failed",
message: "Browser.getVersion returned no result object"
});
};
const finish = (value) => {
if (settled) return;
settled = true;
clearTimeout(timer);
ws.off("message", onMessage);
ws.close();
resolve(value);
};
const timer = setTimeout(() => {
ws.terminate();
finish({
ok: false,
code: opened ? "websocket_health_command_timeout" : "websocket_handshake_failed",
message: opened ? `Browser.getVersion did not respond within ${timeoutMs}ms` : `WebSocket handshake did not complete within ${timeoutMs}ms`
});
}, Math.max(1, timeoutMs + Math.min(25, timeoutMs)));
ws.once("open", () => {
opened = true;
try {
ws.send(JSON.stringify({
id: 1,
method: "Browser.getVersion"
}));
} catch (err) {
finish({
ok: false,
code: "websocket_health_command_failed",
message: safeChromeCdpErrorMessage(err)
});
}
});
ws.on("message", onMessage);
ws.once("error", (err) => {
finish({
ok: false,
code: opened ? "websocket_health_command_failed" : "websocket_handshake_failed",
message: safeChromeCdpErrorMessage(err)
});
});
ws.once("close", () => {
finish({
ok: false,
code: opened ? "websocket_health_command_failed" : "websocket_handshake_failed",
message: opened ? "WebSocket closed before Browser.getVersion completed" : "WebSocket closed before handshake completed"
});
});
});
}
function classifyChromeVersionError(error) {
const message = safeChromeCdpErrorMessage(error);
if (error instanceof BrowserCdpEndpointBlockedError) return {
code: "ssrf_blocked",
message
};
if (/^HTTP \d+/.test(message)) return {
code: "http_status_failed",
message
};
if (error instanceof SyntaxError || message.includes("cdp-version: malformed JSON response") || message.includes("non-object JSON")) return {
code: "invalid_json",
message
};
return {
code: "http_unreachable",
message
};
}
/** Format a Chrome CDP diagnostic result for status and doctor output. */
function formatChromeCdpDiagnostic(diagnostic) {
const redactedCdpUrl = redactCdpUrl(diagnostic.cdpUrl) ?? diagnostic.cdpUrl;
const redactedWsUrl = redactCdpUrl(diagnostic.wsUrl) ?? diagnostic.wsUrl;
if (diagnostic.ok) {
const browser = diagnostic.browser ? ` browser=${diagnostic.browser}` : "";
return `CDP diagnostic: ready after ${diagnostic.elapsedMs}ms; cdp=${redactedCdpUrl}; websocket=${redactedWsUrl}.${browser}`;
}
const websocket = redactedWsUrl ? `; websocket=${redactedWsUrl}` : "";
const wslPortproxyHint = diagnostic.code === "http_unreachable" && isLikelyEmptyHttpReply(diagnostic.message) ? WSL_EMPTY_REPLY_PORTPROXY_HINT : "";
return `CDP diagnostic: ${diagnostic.code} after ${diagnostic.elapsedMs}ms; cdp=${redactedCdpUrl}${websocket}; ${diagnostic.message}.${wslPortproxyHint}`;
}
const WSL_EMPTY_REPLY_PORTPROXY_HINT = " In WSL2-to-Windows Chrome setups, an empty CDP reply can mean netsh is forwarding to the wrong loopback address. On Windows, inspect `netstat -ano | findstr :9222` and `netsh interface portproxy show all`, then curl both 127.0.0.1 and [::1]. Chromium prefers 127.0.0.1 and falls back to [::1] only when the IPv4 bind fails. If svchost/iphlpsvc owns 127.0.0.1:9222, remove the 127.0.0.1:9222 -> 127.0.0.1:9222 self-loop; if chrome.exe listens only on [::1], use v4tov6 with connectaddress=::1 for the WSL2-reachable listener.";
function isLikelyEmptyHttpReply(message) {
return /empty reply|other side closed|socket closed|connection reset|econnreset|terminated before response/i.test(message);
}
async function diagnoseCdpWebSocketEndpoint(params) {
const health = await diagnoseCdpHealthCommand(params.wsUrl, params.handshakeTimeoutMs, params.lookup);
if (!health.ok) return failureDiagnostic({
cdpUrl: params.cdpUrl,
wsUrl: params.wsUrl,
code: health.code,
message: health.message,
startedAt: params.startedAt
});
return {
ok: true,
cdpUrl: params.cdpUrl,
wsUrl: params.wsUrl,
browser: params.version?.Browser ?? health.version?.Browser,
userAgent: params.version?.["User-Agent"] ?? health.version?.["User-Agent"],
elapsedMs: elapsedSince(params.startedAt)
};
}
/** Run HTTP and WebSocket health diagnostics for a Chrome CDP endpoint. */
async function diagnoseChromeCdp(cdpUrl, timeoutMs = 500, handshakeTimeoutMs = 800, ssrfPolicy) {
const startedAt = Date.now();
let configuredPin;
try {
configuredPin = await assertCdpEndpointAllowed(cdpUrl, ssrfPolicy);
} catch (err) {
return failureDiagnostic({
cdpUrl,
code: "ssrf_blocked",
message: safeChromeCdpErrorMessage(err),
startedAt
});
}
const cdpControlPolicy = scopeCdpPolicyToConfiguredEndpoint(cdpUrl, ssrfPolicy);
if (isDirectCdpWebSocketEndpoint(cdpUrl)) return await diagnoseCdpWebSocketEndpoint({
cdpUrl,
wsUrl: cdpUrl,
startedAt,
handshakeTimeoutMs,
lookup: configuredPin?.lookup
});
const discoveryUrl = isWebSocketUrl(cdpUrl) ? normalizeCdpHttpBaseForJsonEndpoints(cdpUrl) : cdpUrl;
let version;
try {
version = await readChromeVersionWithCredentialFallback(discoveryUrl, timeoutMs, cdpControlPolicy);
} catch (err) {
if (isWebSocketUrl(cdpUrl)) return await diagnoseCdpWebSocketEndpoint({
cdpUrl,
wsUrl: cdpUrl,
startedAt,
handshakeTimeoutMs,
lookup: configuredPin?.lookup
});
const classified = classifyChromeVersionError(err);
return failureDiagnostic({
cdpUrl,
code: classified.code,
message: classified.message,
startedAt
});
}
const wsUrlRaw = normalizeOptionalString(version.webSocketDebuggerUrl) ?? "";
if (!wsUrlRaw) {
if (isWebSocketUrl(cdpUrl)) return await diagnoseCdpWebSocketEndpoint({
cdpUrl,
wsUrl: cdpUrl,
startedAt,
handshakeTimeoutMs,
lookup: configuredPin?.lookup,
version
});
return failureDiagnostic({
cdpUrl,
code: "missing_websocket_debugger_url",
message: "CDP /json/version did not include webSocketDebuggerUrl",
startedAt
});
}
const wsUrl = normalizeCdpWsUrl(wsUrlRaw, discoveryUrl);
let discoveredPin;
try {
discoveredPin = await assertCdpEndpointAllowed(wsUrl, cdpControlPolicy, {
source: "discovered",
configuredUrl: cdpUrl
});
} catch (err) {
return failureDiagnostic({
cdpUrl,
wsUrl,
code: "websocket_ssrf_blocked",
message: safeChromeCdpErrorMessage(err),
startedAt
});
}
const health = await diagnoseCdpHealthCommand(wsUrl, handshakeTimeoutMs, discoveredPin?.lookup);
if (!health.ok) {
if (isWebSocketUrl(cdpUrl) && wsUrl !== cdpUrl) {
if ((await diagnoseCdpHealthCommand(cdpUrl, handshakeTimeoutMs, configuredPin?.lookup)).ok) return {
ok: true,
cdpUrl,
wsUrl: cdpUrl,
browser: version.Browser,
userAgent: version["User-Agent"],
elapsedMs: elapsedSince(startedAt)
};
}
return failureDiagnostic({
cdpUrl,
wsUrl,
code: health.code,
message: health.message,
startedAt
});
}
return {
ok: true,
cdpUrl,
wsUrl,
browser: version.Browser,
userAgent: version["User-Agent"],
elapsedMs: elapsedSince(startedAt)
};
}
//#endregion
//#region extensions/browser/src/browser/chrome.executables.ts
/**
* Chrome executable discovery and version parsing.
*
* Locates supported Chromium-family executables across platforms and reads
* their version strings for capability checks.
*/
const CHROME_VERSION_RE = /\b(\d+)(?:\.\d+){1,3}\b/g;
const PLAYWRIGHT_BROWSERS_PATH_ENV = "PLAYWRIGHT_BROWSERS_PATH";
const BROWSER_VERSION_TIMEOUT_MS = 6e3;
const MAC_PLISTBUDDY_TIMEOUT_MS = 800;
const WINDOWS_FILE_METADATA_TIMEOUT_MS = 4e3;
const DEFAULT_WINDOWS_PROGRAM_FILES = "C:\\Program Files";
const DEFAULT_WINDOWS_PROGRAM_FILES_X86 = "C:\\Program Files (x86)";
const CHROMIUM_BUNDLE_IDS = /* @__PURE__ */ new Set([
"com.google.Chrome",
"com.google.Chrome.beta",
"com.google.Chrome.canary",
"com.google.Chrome.dev",
"com.brave.Browser",
"com.brave.Browser.beta",
"com.brave.Browser.nightly",
"com.microsoft.Edge",
"com.microsoft.EdgeBeta",
"com.microsoft.EdgeDev",
"com.microsoft.EdgeCanary",
"com.microsoft.edgemac",
"com.microsoft.edgemac.beta",
"com.microsoft.edgemac.dev",
"com.microsoft.edgemac.canary",
"org.chromium.Chromium",
"com.vivaldi.Vivaldi",
"com.operasoftware.Opera",
"com.operasoftware.OperaGX",
"com.yandex.desktop.yandex-browser",
"company.thebrowser.Browser"
]);
const CHROMIUM_DESKTOP_IDS = /* @__PURE__ */ new Set([
"google-chrome.desktop",
"google-chrome-beta.desktop",
"google-chrome-unstable.desktop",
"brave-browser.desktop",
"microsoft-edge.desktop",
"microsoft-edge-beta.desktop",
"microsoft-edge-dev.desktop",
"microsoft-edge-canary.desktop",
"chromium.desktop",
"chromium-browser.desktop",
"vivaldi.desktop",
"vivaldi-stable.desktop",
"opera.desktop",
"opera-gx.desktop",
"yandex-browser.desktop",
"org.chromium.Chromium.desktop"
]);
const CHROMIUM_EXE_NAMES = /* @__PURE__ */ new Set([
"chrome.exe",
"msedge.exe",
"brave.exe",
"brave-browser.exe",
"chromium.exe",
"vivaldi.exe",
"opera.exe",
"yandex.exe",
"yandexbrowser.exe",
"google chrome",
"google chrome canary",
"brave browser",
"microsoft edge",
"chromium",
"chrome",
"brave",
"msedge",
"brave-browser",
"google-chrome",
"google-chrome-stable",
"google-chrome-beta",
"google-chrome-unstable",
"microsoft-edge",
"microsoft-edge-beta",
"microsoft-edge-dev",
"microsoft-edge-canary",
"chromium-browser",
"vivaldi",
"vivaldi-stable",
"opera",
"opera-stable",
"opera-gx",
"yandex-browser"
]);
function exists$1(filePath) {
try {
return fs.existsSync(filePath);
} catch {
return false;
}
}
function isExecutable(filePath, platform) {
try {
if (!fs.statSync(filePath).isFile()) return false;
fs.accessSync(filePath, platform === "win32" ? fs.constants.F_OK : fs.constants.X_OK);
return true;
} catch {
return false;
}
}
function execText(command, args, timeoutMs = 1200, maxBuffer = 1048576) {
try {
const output = execFileSync(command, args, {
timeout: timeoutMs,
encoding: "utf8",
maxBuffer
});
return normalizeOptionalString(output) ?? null;
} catch {
return null;
}
}
function inferKindFromIdentifier(identifier) {
const id = normalizeLowercaseStringOrEmpty(identifier);
if (id.includes("brave")) return "brave";
if (id.includes("edge")) return "edge";
if (id.includes("chromium")) return "chromium";
if (id.includes("canary")) return "canary";
if (id.includes("opera") || id.includes("vivaldi") || id.includes("yandex") || id.includes("thebrowser")) return "chromium";
return "chrome";
}
function inferKindFromExecutableName(name) {
const lower = normalizeLowercaseStringOrEmpty(name);
if (lower.includes("brave")) return "brave";
if (lower.includes("edge") || lower.includes("msedge")) return "edge";
if (lower.includes("chromium")) return "chromium";
if (lower.includes("canary") || lower.includes("sxs")) return "canary";
if (lower.includes("opera") || lower.includes("vivaldi") || lower.includes("yandex")) return "chromium";
return "chrome";
}
function detectDefaultChromiumExecutable(platform) {
if (platform === "darwin") return detectDefaultChromiumExecutableMac();
if (platform === "linux") return detectDefaultChromiumExecutableLinux();
if (platform === "win32") return detectDefaultChromiumExecutableWindows();
return null;
}
function detectDefaultChromiumExecutableMac() {
const bundleId = detectDefaultBrowserBundleIdMac();
if (!bundleId || !CHROMIUM_BUNDLE_IDS.has(bundleId)) return null;
const appPathRaw = execText("/usr/bin/osascript", ["-e", `POSIX path of (path to application id "${bundleId}")`]);
if (!appPathRaw) return null;
const appPath = appPathRaw.replace(/\/$/, "");
const exeName = execText("/usr/bin/defaults", [
"read",
path.join(appPath, "Contents", "Info"),
"CFBundleExecutable"
]);
if (!exeName) return null;
const exePath = path.join(appPath, "Contents", "MacOS", exeName);
if (!isExecutable(exePath, "darwin")) return null;
return {
kind: inferKindFromIdentifier(bundleId),
path: exePath
};
}
function detectDefaultBrowserBundleIdMac() {
const plistPath = path.join(os.homedir(), "Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist");
if (!exists$1(plistPath)) return null;
const handlersRaw = execText("/usr/bin/plutil", [
"-extract",
"LSHandlers",
"json",
"-o",
"-",
"--",
plistPath
], 2e3, 5242880);
if (!handlersRaw) return null;
let handlers;
try {
handlers = JSON.parse(handlersRaw);
} catch {
return null;
}
if (!Array.isArray(handlers)) return null;
const resolveScheme = (scheme) => {
let candidate = null;
for (const entry of handlers) {
if (!entry || typeof entry !== "object") continue;
const record = entry;
if (record.LSHandlerURLScheme !== scheme) continue;
const role = typeof record.LSHandlerRoleAll === "string" && record.LSHandlerRoleAll || typeof record.LSHandlerRoleViewer === "string" && record.LSHandlerRoleViewer || null;
if (role) candidate = role;
}
return candidate;
};
return resolveScheme("http") ?? resolveScheme("https");
}
function detectDefaultChromiumExecutableLinux() {
const desktopId = execText("xdg-settings", ["get", "default-web-browser"]) || execText("xdg-mime", [
"query",
"default",
"x-scheme-handler/http"
]);
if (!desktopId) return null;
const trimmed = desktopId.trim();
if (!CHROMIUM_DESKTOP_IDS.has(trimmed)) return null;
const desktopPath = findDesktopFilePath(trimmed);
if (!desktopPath) return null;
const execLine = readDesktopExecLine(desktopPath);
if (!execLine) return null;
const command = extractExecutableFromExecLine(execLine);
if (!command) return null;
const resolved = resolveLinuxExecutablePath(command);
if (!resolved || !isExecutable(resolved, "linux")) return null;
const exeName = normalizeLowercaseStringOrEmpty(path.posix.basename(resolved));
if (!CHROMIUM_EXE_NAMES.has(exeName)) return null;
return {
kind: inferKindFromExecutableName(exeName),
path: resolved
};
}
function detectDefaultChromiumExecutableWindows() {
const progId = readWindowsProgId();
const command = (progId ? readWindowsCommandForProgId(progId) : null) || readWindowsCommandForProgId("http");
if (!command) return null;
const exePath = extractWindowsExecutablePath(expandWindowsEnvVars(command));
if (!exePath) return null;
if (!isExecutable(exePath, "win32")) return null;
const directPath = resolveDirectWindowsBrowserExecutable(exePath);
if (!directPath) return null;
const exeName = normalizeLowercaseStringOrEmpty(path.win32.basename(directPath));
if (!CHROMIUM_EXE_NAMES.has(exeName)) return null;
return {
kind: inferKindFromExecutableName(exeName),
path: directPath
};
}
/** Resolve launchers that hand off to another process into a directly owned browser binary. */
function resolveDirectWindowsBrowserExecutable(executablePath) {
if (normalizeLowercaseStringOrEmpty(path.win32.basename(executablePath)) !== "launcher.exe") return executablePath;
const installDir = path.win32.dirname(executablePath);
try {
const status = JSON.parse(fs.readFileSync(path.win32.join(installDir, "installation_status.json"), "utf8"));
const subfolder = status && typeof status === "object" ? Reflect.get(status, "_subfolder") : null;
if (typeof subfolder !== "string" || !WINDOWS_VERSION_DIR_RE.test(subfolder)) return null;
const candidate = path.win32.join(installDir, subfolder, "opera.exe");
return exists$1(candidate) ? candidate : null;
} catch {
return null;
}
}
function findDesktopFilePath(desktopId) {
const candidates = [
path.join(os.homedir(), ".local", "share", "applications", desktopId),
path.join("/usr/local/share/applications", desktopId),
path.join("/usr/share/applications", desktopId),
path.join("/var/lib/snapd/desktop/applications", desktopId)
];
for (const candidate of candidates) if (exists$1(candidate)) return candidate;
return null;
}
function readDesktopExecLine(desktopPath) {
try {
const lines = fs.readFileSync(desktopPath, "utf8").split(/\r?\n/);
for (const line of lines) if (line.startsWith("Exec=")) return line.slice(5).trim();
} catch {}
return null;
}
function extractExecutableFromExecLine(execLine) {
const tokens = splitExecLine(execLine);
for (const token of tokens) {
if (!token) continue;
if (token === "env") continue;
if (token.includes("=") && !token.startsWith("/") && !token.includes("\\")) continue;
return token.replace(/^["']|["']$/g, "");
}
return null;
}
function splitExecLine(line) {
const tokens = [];
let current = "";
let inQuotes = false;
let quoteChar = "";
for (const ch of line) {
if ((ch === "\"" || ch === "'") && (!inQuotes || ch === quoteChar)) {
if (inQuotes) {
inQuotes = false;
quoteChar = "";
} else {
inQuotes = true;
quoteChar = ch;
}
continue;
}
if (!inQuotes && /\s/.test(ch)) {
if (current) {
tokens.push(current);
current = "";
}
continue;
}
current += ch;
}
if (current) tokens.push(current);
return tokens;
}
function resolveLinuxExecutablePath(command) {
const cleaned = command.trim().replace(/%[a-zA-Z]/g, "");
if (!cleaned) return null;
if (cleaned.startsWith("/")) return cleaned;
const resolved = execText("which", [cleaned], 800);
return resolved ? resolved.trim() : null;
}
function readWindowsProgId() {
const output = execText("reg", [
"query",
"HKCU\\Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\http\\UserChoice",
"/v",
"ProgId"
]);
if (!output) return null;
return output.match(/ProgId\s+REG_\w+\s+(.+)$/im)?.[1]?.trim() || null;
}
function readWindowsCommandForProgId(progId) {
const output = execText("reg", [
"query",
progId === "http" ? "HKCR\\http\\shell\\open\\command" : `HKCR\\${progId}\\shell\\open\\command`,
"/ve"
]);
if (!output) return null;
const match = output.match(/REG_\w+\s+(.+)$/im);
return normalizeOptionalString(match?.[1]) ?? null;
}
function resolveWindowsBrowserInstallRoots() {
return {
localAppData: normalizeOptionalString(process.env.LOCALAPPDATA) ?? path.win32.join(os.homedir(), "AppData", "Local"),
programFiles: normalizeOptionalString(process.env.ProgramFiles) ?? DEFAULT_WINDOWS_PROGRAM_FILES,
programFilesX86: normalizeOptionalString(process.env["ProgramFiles(x86)"]) ?? DEFAULT_WINDOWS_PROGRAM_FILES_X86
};
}
function expandWindowsEnvVars(value) {
const installRoots = resolveWindowsBrowserInstallRoots();
const installRootByEnvName = {
localappdata: installRoots.localAppData,
programfiles: installRoots.programFiles,
"programfiles(x86)": installRoots.programFilesX86
};
return value.replace(/%([^%]+)%/g, (_match, name) => {
const key = normalizeOptionalString(name);
if (!key) return _match;
return normalizeOptionalString(process.env[key]) ?? installRootByEnvName[key.toLowerCase()] ?? `%${key}%`;
});
}
function extractWindowsExecutablePath(command) {
const quoted = command.match(/"([^"]+\.exe)"/i);
if (quoted?.[1]) return quoted[1];
const unquoted = command.match(/^\s*(\S+\.exe)(?:\s|$)/i);
if (unquoted?.[1]) return unquoted[1];
return null;
}
function findFirstExecutable(candidates, platform) {
for (const candidate of candidates) if (isExecutable(candidate.path, platform)) return candidate;
return null;
}
function findFirstChromeExecutable(candidates, platform) {
for (const candidate of candidates) if (isExecutable(candidate, platform)) {
const normalizedPath = normalizeLowercaseStringOrEmpty(candidate);
return {
kind: normalizedPath.includes("beta") || normalizedPath.includes("canary") || normalizedPath.includes("sxs") || normalizedPath.includes("unstable") ? "canary" : "chrome",
path: candidate
};
}
return null;
}
function findPlaywrightChromiumExecutableCandidatesLinux() {
const candidates = [];
for (const browserPath of getPlaywrightBrowserCachePaths()) for (const entry of readSortedDirNames(browserPath)) {
if (!entry.startsWith("chromium-")) continue;
for (const linuxDir of ["chrome-linux64", "chrome-linux"]) candidates.push({
kind: "chromium",
path: path.join(browserPath, entry, linuxDir, "chrome")
});
}
return candidates;
}
function getPlaywrightBrowserCachePaths() {
const configured = normalizeOptionalString(process.env[PLAYWRIGHT_BROWSERS_PATH_ENV]);
const candidates = [configured && configured !== "0" ? configured : null, path.join(os.homedir(), ".cache", "ms-playwright")];
const seen = /* @__PURE__ */ new Set();
return candidates.filter((candidate) => {
if (!candidate || seen.has(candidate)) return false;
seen.add(candidate);
return true;
});
}
function readSortedDirNames(dir) {
try {
return fs.readdirSync(dir).toSorted();
} catch {
return [];
}
}
/** Find the best Chromium-family executable on macOS. */
function findChromeExecutableMac() {
const applications = [
["chrome", "Google Chrome"],
["brave", "Brave Browser"],
["edge", "Microsoft Edge"],
["chromium", "Chromium"],
["canary", "Google Chrome Canary"]
];
const roots = ["/Applications", path.join(os.homedir(), "Applications")];
return findFirstExecutable(applications.flatMap(([kind, name]) => roots.map((root) => ({
kind,
path: path.join(root, `${name}.app`, "Contents", "MacOS", name)
}))), "darwin");
}
function findGoogleChromeExecutableMac() {
return findFirstChromeExecutable([
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
path.join(os.homedir(), "Applications/Google Chrome.app/Contents/MacOS/Google Chrome"),
"/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",
path.join(os.homedir(), "Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary")
], "darwin");
}
/** Find the best Chromium-family executable on Linux. */
function findChromeExecutableLinux() {
return findFirstExecutable([
{
kind: "chrome",
path: "/usr/bin/google-chrome"
},
{
kind: "chrome",
path: "/usr/bin/google-chrome-stable"
},
{
kind: "chrome",
path: "/usr/bin/chrome"
},
{
kind: "chrome",
path: "/opt/google/chrome/chrome"
},
{
kind: "brave",
path: "/usr/bin/brave-browser"
},
{
kind: "brave",
path: "/usr/bin/brave-browser-stable"
},
{
kind: "brave",
path: "/usr/bin/brave"
},
{
kind: "brave",
path: "/snap/bin/brave"
},
{
kind: "brave",
path: "/opt/brave.com/brave/brave-browser"
},
{
kind: "edge",
path: "/usr/bin/microsoft-edge"
},
{
kind: "edge",
path: "/usr/bin/microsoft-edge-stable"
},
{
kind: "chromium",
path: "/usr/bin/chromium"
},
{
kind: "chromium",
path: "/usr/bin/chromium-browser"
},
{
kind: "chromium",
path: "/usr/lib/chromium/chromium"
},
{
kind: "chromium",
path: "/usr/lib/chromium-browser/chromium-browser"
},
{
kind: "chromium",
path: "/snap/bin/chromium"
},
...findPlaywrightChromiumExecutableCandidatesLinux()
], "linux");
}
function findGoogleChromeExecutableLinux() {
return findFirstChromeExecutable([
"/usr/bin/google-chrome",
"/usr/bin/google-chrome-stable",
"/usr/bin/google-chrome-beta",
"/usr/bin/google-chrome-unstable",
"/opt/google/chrome/chrome",
"/snap/bin/google-chrome"
], "linux");
}
/** Find the best Chromium-family executable on Windows. */
function findChromeExecutableWindows() {
const { localAppData, programFiles, programFilesX86 } = resolveWindowsBrowserInstallRoots();
const browsers = [
[
"chrome",
"Google",
"Chrome",
"Application",
"chrome.exe"
],
[
"brave",
"BraveSoftware",
"Brave-Browser",
"Application",
"brave.exe"
],
[
"edge",
"Microsoft",
"Edge",
"Application",
"msedge.exe"
],
[
"chromium",
"Chromium",
"Application",
"chrome.exe"
],
[
"canary",
"Google",
"Chrome SxS",
"Application",
"chrome.exe"
]
];
const candidates = localAppData ? browsers.map(([kind, ...segments]) => ({
kind,
path: path.win32.join(localAppData, ...segments)
})) : [];
for (const [kind, ...segments] of browsers.slice(0, 3)) for (const root of [programFiles, programFilesX86]) candidates.push({
kind,
path: path.win32.join(root, ...segments)
});
return findFirstExecutable(candidates, "win32");
}
function findGoogleChromeExecutableWindows() {
const { localAppData, programFiles, programFilesX86 } = resolveWindowsBrowserInstallRoots();
const joinWin = path.win32.join;
const candidates = [];
if (localAppData) {
candidates.push(joinWin(localAppData, "Google", "Chrome", "Application", "chrome.exe"));
candidates.push(joinWin(localAppData, "Google", "Chrome SxS", "Application", "chrome.exe"));
}
candidates.push(joinWin(programFiles, "Google", "Chrome", "Application", "chrome.exe"));
candidates.push(joinWin(programFilesX86, "Google", "Chrome", "Application", "chrome.exe"));
return findFirstChromeExecutable(candidates, "win32");
}
/** Resolve the Google Chrome executable for a named platform when available. */
function resolveGoogleChromeExecutableForPlatform(platform) {
if (platform === "darwin") return findGoogleChromeExecutableMac();
if (platform === "linux") return findGoogleChromeExecutableLinux();
if (platform === "win32") return findGoogleChromeExecutableWindows();
return null;
}
/** Read a browser executable version from platform metadata or a command-line probe. */
function readBrowserVersion(executablePath) {
if (process.platform === "darwin") {
const bundleVersion = readMacBundleBrowserVersion(executablePath);
if (bundleVersion) return bundleVersion;
}
if (process.platform === "win32") return readWindowsBrowserVersion(executablePath);
const output = execText(executablePath, ["--version"], BROWSER_VERSION_TIMEOUT_MS);
if (!output) return null;
return output.replace(/\s+/g, " ").trim();
}
function readMacBundleBrowserVersion(executablePath) {
const appBundlePath = resolveMacAppBundlePath(executablePath);
if (!appBundlePath) return null;
return execText("/usr/libexec/PlistBuddy", [
"-c",
"Print :CFBundleShortVersionString",
path.join(appBundlePath, "Contents", "Info.plist")
], MAC_PLISTBUDDY_TIMEOUT_MS);
}
const WINDOWS_VERSION_DIR_RE = /^\d+(?:\.\d+){1,3}$/;
function readWindowsBrowserVersion(executablePath) {
const configuredSystemRoot = normalizeOptionalString(process.env.SystemRoot);
const systemRoot = configuredSystemRoot && path.win32.isAbsolute(configuredSystemRoot) ? configuredSystemRoot : "C:\\Windows";
const metadataVersion = execText(path.win32.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe"), [
"-NoProfile",
"-NonInteractive",
"-Command",
"[System.Diagnostics.FileVersionInfo]::GetVersionInfo($args[0]).ProductVersion",
executablePath
], WINDOWS_FILE_METADATA_TIMEOUT_MS);
if (metadataVersion) return metadataVersion.replace(/\s+/g, " ").trim();
try {
const versionDirs = fs.readdirSync(path.win32.dirname(executablePath), { withFileTypes: true }).filter((entry) => entry.isDirectory() && WINDOWS_VERSION_DIR_RE.test(entry.name));
return versionDirs.length === 1 ? versionDirs[0]?.name ?? null : null;
} catch {
return null;
}
}
function resolveMacAppBundlePath(executablePath) {
const parts = path.normalize(executablePath).split(path.sep);
const appIndex = parts.findIndex((part) => part.endsWith(".app"));
if (appIndex < 0) return null;
return parts.slice(0, appIndex + 1).join(path.sep) || path.sep;
}
/** Parse a major browser version from a raw version string. */
function parseBrowserMajorVersion(rawVersion) {
const match = [...(rawVersion ?? "").matchAll(CHROME_VERSION_RE)].at(-1);
if (!match?.[1]) return null;
const major = Number.parseInt(match[1], 10);
return Number.isFinite(major) ? major : null;
}
/** Resolve the preferred Chromium-family executable for a platform. */
function resolveBrowserExecutableForPlatform(resolved, platform) {
if (resolved.executablePath) {
if (!exists$1(resolved.executablePath)) throw new Error(`browser.executablePath not found: ${resolved.executablePath}`);
const directPath = platform === "win32" ? resolveDirectWindowsBrowserExecutable(resolved.executablePath) : resolved.executablePath;
if (!directPath) throw new Error(`browser.executablePath must point to the browser executable, not a handoff launcher: ${resolved.executablePath}`);
return {
kind: "custom",
path: directPath
};
}
const detected = detectDefaultChromiumExecutable(platform);
if (detected) return detected;
if (platform === "darwin") return findChromeExecutableMac();
if (platform === "linux") return findChromeExecutableLinux();
if (platform === "win32") return findChromeExecutableWindows();
return null;
}
//#endregion
//#region extensions/browser/src/browser/chrome.profile-decoration.ts
/**
* OpenClaw-managed Chrome profile decoration.
*
* Applies managed-browser policy, a stable profile name, color, download
* directory, and clean-exit markers to Chrome's profile files.
*/
const CHROME_NETWORK_PREDICTION_DISABLED = 2;
function decoratedMarkerPath(userDataDir) {
return path.join(userDataDir, ".openclaw-profile-decorated");
}
function safeReadJson(filePath) {
return asNullableRecord(loadJsonFile(filePath));
}
function safeWriteJson(filePath, data) {
saveJsonFile(filePath, data);
}
function readNestedRecord(root, key) {
return asNullableRecord(asNullableRecord(root)?.[key]);
}
function readDefaultProfileInfo(localState) {
return readNestedRecord(readNestedRecord(asNullableRecord(localState)?.profile, "info_cache"), "Default");
}
function setDeep(obj, keys, value) {
if (keys.length === 0) return;
let node = obj;
for (const key of keys.slice(0, -1)) {
const next = node[key];
if (typeof next !== "object" || next === null || Array.isArray(next)) node[key] = {};
node = node[key];
}
const lastKey = keys.at(-1);
if (lastKey !== void 0) node[lastKey] = value;
}
function parseHexRgbToSignedArgbInt(hex) {
const cleaned = hex.trim().replace(/^#/, "");
if (!/^[0-9a-fA-F]{6}$/.test(cleaned)) return null;
const argbUnsigned = 255 << 24 | Number.parseInt(cleaned, 16);
return argbUnsigned > 2147483647 ? argbUnsigned - 4294967296 : argbUnsigned;
}
/** Return true when a managed Chrome profile already has desired decoration. */
function isProfileDecorated(userDataDir, desiredName, desiredColorHex, desiredDownloadDir) {
const desiredColorInt = parseHexRgbToSignedArgbInt(desiredColorHex);
const localStatePath = path.join(userDataDir, "Local State");
const preferencesPath = path.join(userDataDir, "Default", "Preferences");
const info = readDefaultProfileInfo(safeReadJson(localStatePath));
const prefs = safeReadJson(preferencesPath);
const browserTheme = readNestedRecord(prefs?.browser, "theme");
const autogeneratedTheme = readNestedRecord(prefs?.autogenerated, "theme");
const download = readNestedRecord(prefs, "download");
const savefile = readNestedRecord(prefs, "savefile");
const nameOk = typeof info?.name === "string" ? info.name === desiredName : true;
const downloadOk = desiredDownloadDir ? download?.default_directory === desiredDownloadDir && download.prompt_for_download === false && download.directory_upgrade === true && savefile?.default_directory === desiredDownloadDir : true;
if (desiredColorInt == null) return nameOk && downloadOk;
const localSeedOk = typeof info?.profile_color_seed === "number" ? info.profile_color_seed === desiredColorInt : false;
const prefOk = typeof browserTheme?.user_color2 === "number" && browserTheme.user_color2 === desiredColorInt || typeof autogeneratedTheme?.color === "number" && autogeneratedTheme.color === desiredColorInt;
return nameOk && localSeedOk && prefOk && downloadOk;
}
/** Return whether this profile was initialized with Chromium's automation keychain. */
function usesOpenClawMockKeychain(userDataDir) {
return readDefaultProfileInfo(safeReadJson(path.join(userDataDir, "Local State")))?.openclaw_mock_keychain === true;
}
/** Disable Chromium network prediction in an OpenClaw-managed Chrome profile. */
function ensureProfileNetworkPredictionDisabled(userDataDir) {
const preferencesPath = path.join(userDataDir, "Default", "Preferences");
const prefs = safeReadJson(preferencesPath) ?? {};
setDeep(prefs, ["net", "network_prediction_options"], CHROME_NETWORK_PREDICTION_DISABLED);
safeWriteJson(preferencesPath, prefs);
}
/**
* Best-effort profile decoration (name + lobster-orange). Chrome preference keys
* vary by version; we keep this conservative and idempotent.
*/
function decorateOpenClawProfile(userDataDir, opts) {
const desiredName = opts?.name ?? "openclaw";
const desiredColor = (opts?.color ?? "#FF4500").toUpperCase();
const desiredColorInt = parseHexRgbToSignedArgbInt(desiredColor);
const localStatePath = path.join(userDataDir, "Local State");
const preferencesPath = path.join(userDataDir, "Default", "Preferences");
const localState = safeReadJson(localStatePath) ?? {};
setDeep(localState, [
"profile",
"info_cache",
"Default",
"name"
], desiredName);
setDeep(localState, [
"profile",
"info_cache",
"Default",
"shortcut_name"
], desiredName);
setDeep(localState, [
"profile",
"info_cache",
"Default",
"user_name"
], desiredName);
if (opts?.mockKeychain) setDeep(localState, [
"profile",
"info_cache",
"Default",
"openclaw_mock_keychain"
], true);
setDeep(localState, [
"profile",
"info_cache",
"Default",
"profile_color"
], desiredColor);
setDeep(localState, [
"profile",
"info_cache",
"Default",
"user_color"
], desiredColor);
if (desiredColorInt != null) {
setDeep(localState, [
"profile",
"info_cache",
"Default",
"profile_color_seed"
], desiredColorInt);
setDeep(localState, [
"profile",
"info_cache",
"Default",
"profile_highlight_color"
], desiredColorInt);
setDeep(localState, [
"profile",
"info_cache",
"Default",
"default_avatar_fill_color"
], desiredColorInt);
setDeep(localState, [
"profile",
"info_cache",
"Default",
"default_avatar_stroke_color"
], desiredColorInt);
}
safeWriteJson(localStatePath, localState);
const prefs = safeReadJson(preferencesPath) ?? {};
setDeep(prefs, ["profile", "name"], desiredName);
setDeep(prefs, ["profile", "profile_color"], desiredColor);
setDeep(prefs, ["profile", "user_color"], desiredColor);
if (desiredColorInt != null) {
setDeep(prefs, [
"autogenerated",
"theme",
"color"
], desiredColorInt);
setDeep(prefs, [
"browser",
"theme",
"user_color2"
], desiredColorInt);
}
if (opts?.downloadDir) {
setDeep(prefs, ["download", "default_directory"], opts.downloadDir);
setDeep(prefs, ["download", "prompt_for_download"], false);
setDeep(prefs, ["download", "directory_upgrade"], true);
setDeep(prefs, ["savefile", "default_directory"], opts.downloadDir);
}
safeWriteJson(preferencesPath, prefs);
try {
fs.writeFileSync(decoratedMarkerPath(userDataDir), `${Date.now()}\n`, "utf-8");
} catch {}
}
/** Mark the managed Chrome profile as cleanly exited. */
function ensureProfileCleanExit(userDataDir) {
const preferencesPath = path.join(userDataDir, "Default", "Preferences");
const prefs = safeReadJson(preferencesPath) ?? {};
setDeep(prefs, ["exit_type"], "Normal");
setDeep(prefs, ["exited_cleanly"], true);
safeWriteJson(preferencesPath, prefs);
}
//#endregion
//#region extensions/browser/src/browser/chrome.ts
/**
* OpenClaw-managed Chrome lifecycle and CDP helpers.
*
* Builds launch args, starts/stops managed Chrome, probes CDP readiness, and
* resolves WebSocket endpoints for browser control.
*/
const log = createSubsystemLogger("browser").child("chrome");
const CHROME_SINGLETON_LOCK_PATHS = [
"SingletonLock",
"SingletonSocket",
"SingletonCookie"
];
const CHROME_SINGLETON_IN_USE_PATTERN = /profile appears to be in use by another chromium process/i;
const CHROME_MISSING_DISPLAY_PATTERN = /missing x server|\$DISPLAY/i;
const CHROME_GRACEFUL_CLOSE_COMMAND_TIMEOUT_MS = 500;
const CHROME_LAUNCH_STDERR_TAIL_MAX_BYTES = 65536;
const CHROME_HTTP_DISCOVERY_FAILURE_CODES = /* @__PURE__ */ new Set([
"ssrf_blocked",
"http_unreachable",
"http_status_failed",
"invalid_json"
]);
const TCP_LISTEN_STATE_HEX = "0A";
function exists(filePath) {
try {
return fs.existsSync(filePath);
} catch {
return false;
}
}
function diagnosticShowsChromeHttpDiscovery(diagnostic) {
if (!diagnostic) return false;
if (diagnostic.ok) return true;
return !CHROME_HTTP_DISCOVERY_FAILURE_CODES.has(diagnostic.code);
}
function createChromeLaunchStderrDiagnostics(maxBytes) {
const tail = createBoundedUtf8Tail(maxBytes);
const signals = {
singletonInUse: false,
missingDisplay: false
};
let markerScanTail = "";
const updateSignals = (chunkText) => {
const scanText = `${markerScanTail}${chunkText}`;
signals.singletonInUse ||= CHROME_SINGLETON_IN_USE_PATTERN.test(scanText);
signals.missingDisplay ||= CHROME_MISSING_DISPLAY_PATTERN.test(scanText);
markerScanTail = scanText.slice(-256);
};
return {
append(chunk) {
tail.append(chunk);
const chunkText = Buffer.isBuffer(chunk) ? chunk.toString("utf8") : chunk;
if (chunkText.length > 0) updateSignals(chunkText);
},
toString() {
return tail.text();
},
signals() {
return { ...signals };
},
clear() {
tail.clear();
signals.singletonInUse = false;
signals.missingDisplay = false;
markerScanTail = "";
}
};
}
function readSingletonLockTarget(userDataDir) {
let target;
try {
target = fs.readlinkSync(path.join(userDataDir, "SingletonLock"));
} catch {
return null;
}
const match = /^(?<lockHost>.+)-(?<pid>\d+)$/.exec(target);
if (!match?.groups) return null;
return {
hostname: normalizeOptionalString(match.groups.lockHost) ?? "",
pid: Number.parseInt(match.groups.pid ?? "", 10)
};
}
function readLinuxProcessStartTime(pid) {
let stat;
try {
stat = fs.readFileSync(`/proc/${pid}/stat`, "utf8");
} catch {
return null;
}
const fields = stat.slice(stat.lastIndexOf(")") + 2).split(/\s+/);
return normalizeOptionalString(fields[19]) ?? null;
}
function readLinuxProcessArgv(pid) {
let cmdline;
try {
cmdline = fs.readFileSync(`/proc/${pid}/cmdline`);
} catch {
return null;
}
const argv = cmdline.toString("utf8").split("\0").filter((arg) => arg.length > 0);
return argv.length > 0 ? argv : null;
}
function readPsCommandLine(pid) {
try {
return normalizeOptionalString(execFileSync("ps", [
"-ww",
"-p",
String(pid),
"-o",
"command="
], {
encoding: "utf8",
timeout: 1e3,
maxBuffer: 65536
})) ?? null;
} catch {
return null;
}
}
function readPsStartTime(pid) {
try {
return normalizeOptionalString(execFileSync("ps", [
"-p",
String(pid),
"-o",
"lstart="
], {
encoding: "utf8",
timeout: 1e3,
maxBuffer: 65536
})) ?? null;
} catch {
return null;
}
}
function readManagedProcessCommandLine(pid) {
if (process.platform === "linux") {
const argv = readLinuxProcessArgv(pid);
if (!argv) return null;
const startTime = readLinuxProcessStartTime(pid);
if (!startTime) return null;
return {
argv,
text: argv.join(" "),
startTime
};
}
if (process.platform === "darwin") {
const text = readPsCommandLine(pid);
const startTime = readPsStartTime(pid);
if (!text || !startTime) return null;
return {
argv: null,
text,
startTime
};
}
return null;
}
function isChromeExecutableFamilyMatch(commandText, exe) {
const normalizedCommand = commandText.toLowerCase();
const configuredPath = exe.path.toLowerCase();
const configuredBase = path.basename(exe.path).toLowerCase();
if (normalizedCommand.includes(configuredPath) || configuredBase.length > 0 && normalizedCommand.includes(configuredBase)) return true;
if (exe.kind === "chrome" || exe.kind === "canary") return /\b(google chrome|google-chrome|chrome|chromium)\b/i.test(commandText);
if (exe.kind === "chromium") return /\b(chromium|chromium-browser)\b/i.test(commandText);
if (exe.kind === "brave") return /\b(brave browser|brave-browser|brave)\b/i.test(commandText);
if (exe.kind === "edge") return /\b(microsoft edge|microsoft-edge|msedge)\b/i.test(commandText);
return false;
}
function processCommandHasArg(command, expected) {
if (command.argv) return command.argv.includes(expected);
return command.text.includes(expected);
}
function commandLineMatchesManagedChrome(params) {
return isChromeExecutableFamilyMatch(params.command.text, params.exe) && processCommandHasArg(params.command, `--remote-debugging-port=${params.profile.cdpPort}`) && processCommandHasArg(params.command, `--user-data-dir=${params.userDataDir}`);
}
function parseLinuxTcpListenInodesForPort(table, port) {
const expectedPort = port.toString(16).toUpperCase().padStart(4, "0");
const inodes = /* @__PURE__ */ new Set();
for (const line of table.split(/\r?\n/).slice(1)) {
const fields = line.trim().split(/\s+/);
const localAddress = fields[1] ?? "";
const state = fields[3] ?? "";
const inode = fields[9] ?? "";
if (localAddress.split(":").at(-1)?.toUpperCase() === expectedPort && state === TCP_LISTEN_STATE_HEX && inode) inodes.add(inode);
}
return inodes;
}
function readLinuxTcpListenInodesForPort(port) {
const inodes = /* @__PURE__ */ new Set();
for (const tablePath of ["/proc/net/tcp", "/proc/net/tcp6"]) try {
for (const inode of parseLinuxTcpListenInodesForPort(fs.readFileSync(tablePath, "utf8"), port)) inodes.add(inode);
} catch {}
return inodes;
}
function linuxPidOwnsAnySocketInode(pid, inodes) {
if (inodes.size === 0) return false;
let descriptors;
try {
descriptors = fs.readdirSync(`/proc/${pid}/fd`);
} catch {
return false;
}
for (const descriptor of descriptors) {
let target;
try {
target = fs.readlinkSync(`/proc/${pid}/fd/${descriptor}`);
} catch {
continue;
}
const match = /^socket:\[(?<inode>\d+)\]$/.exec(target);
if (match?.groups?.inode && inodes.has(match.groups.inode)) return true;
}
return false;
}
function linuxPidListensOnPort(pid, port) {
return linuxPidOwnsAnySocketInode(pid, readLinuxTcpListenInodesForPort(port));
}
function lsofShowsPidListeningOnPort(pid, port) {
try {
return execFileSync("lsof", [
"-nP",
"-a",
"-p",
String(pid),
`-iTCP:${port}`,
"-sTCP:LISTEN",
"-Fp"
], {
encoding: "utf8",
timeout: 1e3,
maxBuffer: 65536
}).split(/\r?\n/).some((line) => line === `p${pid}`);
} catch {
return false;
}
}
function pidListensOnPort(pid, port) {
if (process.platform === "linux") return linuxPidListensOnPort(pid, port);
if (process.platform === "darwin") return lsofShowsPidListeningOnPort(pid, port);
return false;
}
function sameManagedChromeIdentity(a, b) {
return a.pid === b.pid && a.commandLine === b.commandLine && a.startTime === b.startTime;
}
function readOwnedManagedChromeIdentity(params) {
if (!isPidAlive(params.pid) || !pidListensOnPort(params.pid, params.profile.cdpPort)) return null;
const command = readManagedProcessCommandLine(params.pid);
if (!command || !commandLineMatchesManagedChrome({
command,
exe: params.exe,
profile: params.profile,
userDataDir: params.userDataDir
})) return null;
return {
pid: params.pid,
startTime: command.startTime,
commandLine: command.text
};
}
function isPortInUseError(err) {
const errno = err?.code;
const name = err instanceof Error ? err.name : "";
const message = err instanceof Error ? err.message : String(err);
return errno === "EADDRINUSE" || name === "PortInUseError" || /\bEADDRINUSE\b|already in use/i.test(message);
}
function readCurrentHostSingletonPid(userDataDir, hostname = os.hostname()) {
const lock = readSingletonLockTarget(userDataDir);
if (!lock || lock.hostname !== hostname || !isPidAlive(lock.pid)) return null;
return lock.pid;
}
function clearChromeSingletonArtifacts(userDataDir) {
for (const basename of CHROME_SINGLETON_LOCK_PATHS) try {
fs.rmSync(path.join(userDataDir, basename), { force: true });
} catch {}
}
/** Remove stale Chrome singleton lock files from a user-data-dir. */
function clearStaleChromeSingletonLocks(userDataDir, hostname = os.hostname()) {
const lock = readSingletonLockTarget(userDataDir);
if (!lock || lock.hostname === hostname && isPidAlive(lock.pid)) return false;
clearChromeSingletonArtifacts(userDataDir);
return true;
}
async function waitForChromeProcessExit(proc, timeoutMs) {
if (proc.exitCode != null || proc.signalCode != null) return true;
return await new Promise((resolve) => {
const cleanup = () => {
clearTimeout(timer);
proc.off("exit", onExit);
proc.off("close", onExit);
};
const timer = setTimeout(() => {
cleanup();
resolve(false);
}, timeoutMs);
const onExit = () => {
cleanup();
resolve(true);
};
proc.once("exit", onExit);
proc.once("close", onExit);
if (proc.exitCode != null || proc.signalCode != null) onExit();
});
}
async function signalChromeProcess(proc, signal, timeoutMs) {
if (proc.exitCode != null || proc.signalCode != null) return true;
try {
proc.kill(signal);
} catch {}
return await waitForChromeProcessExit(proc, timeoutMs);
}
async function terminateChromeForRetry(proc, userDataDir) {
if (!await signalChromeProcess(proc, "SIGKILL", 5e3)) return false;
clearStaleChromeSingletonLocks(userDataDir);
return true;
}
async function waitForPidExit(pid, timeoutMs) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (!isPidAlive(pid)) return true;
await new Promise((resolve) => {
setTimeout(resolve, 50);
});
}
return !isPidAlive(pid);
}
async function terminateOwnedStaleChromeProcess(params, timeoutMs = CHROME_STOP_TIMEOUT_MS) {
const readCurrentIdentity = () => readOwnedManagedChromeIdentity({
pid: params.identity.pid,
exe: params.exe,
profile: params.profile,
userDataDir: params.userDataDir
});
const beforeSigterm = readCurrentIdentity();
if (!beforeSigterm || !sameManagedChromeIdentity(params.identity, beforeSigterm)) return false;
try {
process.kill(params.identity.pid, "SIGTERM");
} catch {
return false;
}
if (await waitForPidExit(params.identity.pid, timeoutMs)) return true;
const beforeSigkill = readCurrentIdentity();
if (!beforeSigkill || !sameManagedChromeIdentity(params.identity, beforeSigkill)) return false;
try {
process.kill(params.identity.pid, "SIGKILL");
} catch {
return false;
}
return await waitForPidExit(params.identity.pid, CHROME_BOOTSTRAP_EXIT_TIMEOUT_MS);
}
function clearRecoveredChromeSingletonArtifacts(userDataDir, pid) {
const lock = readSingletonLockTarget(userDataDir);
if (!lock || lock.hostname !== os.hostname() || lock.pid !== pid || isPidAlive(pid)) return false;
clearChromeSingletonArtifacts(userDataDir);
return true;
}
async function recoverOwnedStaleManagedChromeCdpListener(params) {
if (!params.profile.cdpIsLoopback) return false;
const pid = readCurrentHostSingletonPid(params.userDataDir);
if (pid == null) return false;
let diagnostic;
try {
diagnostic = await diagnoseChromeCdp(params.profile.cdpUrl, 500, 800);
} catch {
return false;
}
if (diagnostic.ok || diagnostic.code !== "websocket_health_command_timeout") return false;
const identity = readOwnedManagedChromeIdentity({
pid,
exe: params.exe,
profile: params.profile,
userDataDir: params.userDataDir
});
if (!identity) return false;
if (!await terminateOwnedStaleChromeProcess({
identity,
exe: params.exe,
profile: params.profile,
userDataDir: params.userDataDir
})) return false;
if (!clearRecoveredChromeSingletonArtifacts(params.userDataDir, pid)) return false;
log.warn(`Stopped stale managed Chrome CDP listener for profile "${params.profile.name}" (pid ${pid}) and retrying launch.`);
return true;
}
async function ensureManagedChromePortAvailable(resolved, profile, userDataDir) {
const configuredHost = new URL(profile.cdpUrl).hostname.replace(/^\[|\]$/g, "");
const probeHosts = configuredHost === "127.0.0.1" ? [configuredHost] : ["127.0.0.1", configuredHost];
const ensureProbeHostsAvailable = async () => {
for (const host of probeHosts) await ensurePortAvailable(profile.cdpPort, host);
};
try {
await ensureProbeHostsAvailable();
return;
} catch (err) {
const exe = resolveBrowserExecutable(resolved, profile);
if (!isPortInUseError(err) || !exe) throw err;
if (!await recoverOwnedStaleManagedChromeCdpListener({
exe,
profile,
userDataDir
})) throw err;
}
await ensureProbeHostsAvailable();
}
function chromeLaunchHints(params) {
const hints = [];
if (process.platform === "linux" && !params.resolved.noSandbox) hints.push("If running in a container or as root, try setting browser.noSandbox: true.");
const headlessMode = resolveManagedBrowserHeadlessMode(params.resolved, params.profile, params.launchOptions);
if ((params.stderrSignals?.missingDisplay ?? CHROME_MISSING_DISPLAY_PATTERN.test(params.stderrOutput)) && !headlessMode.headless) hints.push("No DISPLAY/X server was detected. Set OPENCLAW_BROWSER_HEADLESS=1, remove the headed override, start Xvfb, or run the Gateway in a desktop session.");
if (params.stderrSignals?.singletonInUse ?? CHROME_SINGLETON_IN_USE_PATTERN.test(params.stderrOutput)) hints.push(`The Chromium profile "${params.profile.name}" is locked. Stop the existing browser or remove stale Singleton* lock files under ~/.openclaw/browser/${params.profile.name}/user-data.`);
return hints.length > 0 ? `\nHint: ${hints.join("\nHint: ")}` : "";
}
/** A managed child survived bounded cancellation and remains actor-owned for retry. */
var ManagedChromeCleanupError = class extends Error {
constructor(message, running) {
super(message);
this.running = running;
this.code = "MANAGED_CHROME_CLEANUP_FAILED";
this.name = "ManagedChromeCleanupError";
}
};
function resolveBrowserExecutable(resolved, profile) {
return resolveBrowserExecutableForPlatform({
...resolved,
executablePath: profile.executablePath ?? resolved.executablePath
}, process.platform);
}
/** Resolve the user-data-dir path for a managed OpenClaw Chrome profile. */
function resolveOpenClawUserDataDir(profileName = DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME) {
return path.join(CONFIG_DIR, "browser", profileName, "user-data");
}
function cdpUrlForPort(cdpPort) {
return `http://127.0.0.1:${cdpPort}`;
}
/** Build Chrome launch arguments for the managed OpenClaw browser. */
function buildOpenClawChromeLaunchArgs(params) {
const { resolved, profile, userDataDir } = params;
const platform = params.platform ?? process.platform;
const headlessMode = resolveManagedBrowserHeadlessMode(resolved, profile, params);
const args = [
`--remote-debugging-port=${profile.cdpPort}`,
`--user-data-dir=${userDataDir}`,
"--no-first-run",
"--no-default-browser-check",
"--disable-sync",
"--disable-background-networking",
"--disable-component-update",
"--disable-features=Translate,MediaRouter",
"--disable-session-crashed-bubble",
"--hide-crash-restore-bubble",
"--password-store=basic"
];
if (platform === "darwin" && params.useMockKeychain) args.push("--use-mock-keychain");
if (headlessMode.headless) {
args.push("--headless=new");
args.push("--disable-gpu");
}
if (resolved.noSandbox) args.push("--no-sandbox");
if (platform === "linux") args.push("--disable-dev-shm-usage");
if (!hasChromeProxyControlArg(resolved.extraArgs)) args.push("--no-proxy-server");
if (resolved.extraArgs.length > 0) args.push(...resolved.extraArgs);
return args;
}
async function canOpenWebSocket(url, timeoutMs, lookup) {
return new Promise((resolve) => {
const ws = openCdpWebSocket(url, {
handshakeTimeoutMs: timeoutMs,
lookup
});
ws.once("open", () => {
ws.close();
resolve(true);
});
ws.once("error", () => resolve(false));
ws.once("close", () => resolve(false));
});
}
/** Return true when a Chrome CDP endpoint is reachable over HTTP. */
async function isChromeReachable(cdpUrl, timeoutMs = 500, ssrfPolicy) {
try {
const configuredPin = await assertCdpEndpointAllowed(cdpUrl, ssrfPolicy);
if (isDirectCdpWebSocketEndpoint(cdpUrl)) return await canOpenWebSocket(cdpUrl, timeoutMs, configuredPin?.lookup);
if (await fetchChromeVersion(isWebSocketUrl(cdpUrl) ? normalizeCdpHttpBaseForJsonEndpoints(cdpUrl) : cdpUrl, timeoutMs, ssrfPolicy)) return true;
if (isWebSocketUrl(cdpUrl)) return await canOpenWebSocket(cdpUrl, timeoutMs, configuredPin?.lookup);
return false;
} catch {
return false;
}
}
async function fetchChromeVersion(cdpUrl, timeoutMs = 500, ssrfPolicy) {
try {
return await readChromeVersionWithCredentialFallback(cdpUrl, timeoutMs, ssrfPolicy);
} catch {
return null;
}
}
/** Resolve a usable Chrome DevTools WebSocket endpoint from a CDP endpoint. */
async function getChromeWebSocketEndpoint(cdpUrl, timeoutMs = 500, ssrfPolicy) {
const configuredPin = await assertCdpEndpointAllowed(cdpUrl, ssrfPolicy);
const cdpControlPolicy = scopeCdpPolicyToConfiguredEndpoint(cdpUrl, ssrfPolicy);
if (isDirectCdpWebSocketEndpoint(cdpUrl)) return {
url: cdpUrl,
lookup: configuredPin?.lookup
};
const discoveryUrl = isWebSocketUrl(cdpUrl) ? normalizeCdpHttpBaseForJsonEndpoints(cdpUrl) : cdpUrl;
const version = await fetchChromeVersion(discoveryUrl, timeoutMs, cdpControlPolicy);
const wsUrl = normalizeOptionalString(version?.webSocketDebuggerUrl) ?? "";
if (!wsUrl) {
if (isWebSocketUrl(cdpUrl)) return {
url: cdpUrl,
lookup: configuredPin?.lookup
};
return null;
}
const normalizedWsUrl = normalizeCdpWsUrl(wsUrl, discoveryUrl);
return {
url: normalizedWsUrl,
lookup: (await assertCdpEndpointAllowed(normalizedWsUrl, cdpControlPolicy, {
source: "discovered",
configuredUrl: cdpUrl
}))?.lookup
};
}
/** Return true when a Chrome CDP endpoint has a healthy WebSocket command path. */
async function isChromeCdpReady(cdpUrl, timeoutMs = 500, handshakeTimeoutMs = 800, ssrfPolicy) {
const diagnostic = await diagnoseChromeCdp(cdpUrl, timeoutMs, handshakeTimeoutMs, ssrfPolicy);
if (!diagnostic.ok) log.debug(formatChromeCdpDiagnostic(diagnostic));
return diagnostic.ok;
}
async function waitForManagedLaunchPoll(delayMs, signal) {
signal?.throwIfAborted();
try {
await setTimeout$1(delayMs, void 0, signal ? { signal } : void 0);
} catch (err) {
signal?.throwIfAborted();
throw err;
}
}
/** Launch or attach to the managed OpenClaw Chrome profile. */
async function launchOpenClawChrome(resolved, profile, launchOptions = {}) {
const { signal, ...headlessOptions } = launchOptions;
signal?.throwIfAborted();
if (!profile.cdpIsLoopback) throw new Error(`Profile "${profile.name}" is remote; cannot launch local Chrome.`);
const headlessMode = resolveManagedBrowserHeadlessMode(resolved, profile, headlessOptions);
const missingDisplayError = getManagedBrowserMissingDisplayError(resolved, profile, headlessOptions);
if (missingDisplayError) throw new BrowserProfileUnavailableError(missingDisplayError.message, { metadata: {
reason: BROWSER_ERROR_REASONS.noDisplayForHeadedProfile,
details: {
profile: profile.name,
requestedHeadless: false,
headlessSource: missingDisplayError.headlessSource,
displayPresent: false
}
} });
try {
assertManagedProxyAllowsCdpUrl(profile.cdpUrl);
} catch (err) {
throw new BrowserProfileUnavailableError(`Browser profile "${profile.name}" cannot launch: ${err instanceof Error ? err.message : String(err)}`);
}
const userDataDir = resolveOpenClawUserDataDir(profile.name);
await ensureManagedChromePortAvailable(resolved, profile, userDataDir);
signal?.throwIfAborted();
const exe = resolveBrowserExecutable(resolved, profile);
if (!exe) throw new Error("No supported browser found (Chrome/Brave/Edge/Chromium on macOS, Linux, or Windows).");
fs.mkdirSync(userDataDir, { recursive: true });
await ensureOutputDirectory(DEFAULT_DOWNLOAD_DIR);
const localStatePath = path.join(userDataDir, "Local State");
const preferencesPath = path.join(userDataDir, "Default", "Preferences");
const profileIsNew = !exists(localStatePath);
const needsBootstrap = profileIsNew || !exists(preferencesPath);
const useMockKeychain = process.platform === "darwin" && (usesOpenClawMockKeychain(userDataDir) || profileIsNew && headlessMode.headless);
const needsDecorate = !isProfileDecorated(userDataDir, profile.name, (profile.color ?? "#FF4500").toUpperCase(), DEFAULT_DOWNLOAD_DIR);
const spawnOnce = async (onStderr) => {
signal?.throwIfAborted();
const args = buildOpenClawChromeLaunchArgs({
resolved,
profile,
userDataDir,
...headlessOptions,
useMockKeychain
});
const env = {
...omitChromeProxyEnv(process.env),
HOME: os.homedir()
};
if (process.platform === "linux") {
const chromiumStateDir = path.join(resolvePreferredOpenClawTmpDir(), ".chromium");
env.XDG_CONFIG_HOME ??= chromiumStateDir;
env.XDG_CACHE_HOME ??= chromiumStateDir;
}
const preparedSpawn = prepareOomScoreAdjustedSpawn(exe.path, args, { env });
const proc = spawn(preparedSpawn.command, preparedSpawn.args, {
stdio: [
"ignore",
"ignore",
"pipe"
],
env: preparedSpawn.env
});
const onAbort = () => {
try {
proc.kill("SIGKILL");
} catch {}
};
signal?.addEventListener("abort", onAbort, { once: true });
if (signal?.aborted) onAbort();
proc.on("error", (err) => {
log.debug(`managed Chrome process error: ${redactToolPayloadText(String(err))}`);
});
if (onStderr) proc.stderr?.on("data", onStderr);
if (proc.pid == null) try {
await once(proc, "spawn");
} catch (err) {
signal?.removeEventListener("abort", onAbort);
if (onStderr) proc.stderr?.off("data", onStderr);
throw err;
}
const pid = proc.pid;
if (pid == null) {
signal?.removeEventListener("abort", onAbort);
if (onStderr) proc.stderr?.off("data", onStderr);
throw new Error("Managed Chrome process spawned without a pid.");
}
return {
pid,
proc,
releaseAbort: () => signal?.removeEventListener("abort", onAbort)
};
};
const startedAt = Date.now();
const runningForProcess = (proc, pid) => ({
pid,
exe,
userDataDir,
cdpPort: profile.cdpPort,
startedAt,
proc,
headless: headlessMode.headless,
headlessSource: headlessMode.source
});
if (needsBootstrap) {
const { pid: bootstrapPid, proc: bootstrap, releaseAbort } = await spawnOnce();
let bootstrapError;
try {
const deadline = Date.now() + CHROME_BOOTSTRAP_PREFS_TIMEOUT_MS;
while (Date.now() < deadline) {
signal?.throwIfAborted();
if (exists(localStatePath) && exists(preferencesPath)) break;
await waitForManagedLaunchPoll(100, signal);
}
} catch (err) {
bootstrapError = err instanceof Error ? err : new Error("Managed Chrome bootstrap failed.", { cause: err });
}
let exited = await signalChromeProcess(bootstrap, "SIGTERM", CHROME_BOOTSTRAP_EXIT_TIMEOUT_MS);
if (!exited) exited = await signalChromeProcess(bootstrap, "SIGKILL", CHROME_BOOTSTRAP_EXIT_TIMEOUT_MS);
releaseAbort();
if (!exited) throw new ManagedChromeCleanupError(`Managed Chrome bootstrap ${bootstrapPid} survived cleanup.`, runningForProcess(bootstrap, bootstrapPid));
if (bootstrapError) throw bootstrapError;
}
signal?.throwIfAborted();
if (needsDecorate) try {
decorateOpenClawProfile(userDataDir, {
name: profile.name,
color: profile.color,
downloadDir: DEFAULT_DOWNLOAD_DIR,
mockKeychain: useMockKeychain
});
log.info(`🦞 openclaw browser profile decorated (${profile.color})`);
} catch (err) {
log.warn(`openclaw browser profile decoration failed: ${String(err)}`);
}
try {
ensureProfileNetworkPredictionDisabled(userDataDir);
} catch (err) {
log.warn(`openclaw browser network-prediction prefs failed: ${String(err)}`);
}
try {
ensureProfileCleanExit(userDataDir);
} catch (err) {
log.warn(`openclaw browser clean-exit prefs failed: ${String(err)}`);
}
signal?.throwIfAborted();
const launchOnceAndWait = async (allowSingletonRecovery) => {
const stderrDiagnostics = createChromeLaunchStderrDiagnostics(CHROME_LAUNCH_STDERR_TAIL_MAX_BYTES);
const onStderr = (chunk) => {
stderrDiagnostics.append(chunk);
};
let proc;
let releaseSpawnAbort;
try {
const spawned = await spawnOnce(onStderr);
proc = spawned.proc;
releaseSpawnAbort = spawned.releaseAbort;
const readyDeadline = Date.now() + (resolved.localLaunchTimeoutMs ?? CHROME_LAUNCH_READY_WINDOW_MS);
let launchHttpReachable = false;
while (Date.now() < readyDeadline) {
signal?.throwIfAborted();
if (await isChromeReachable(profile.cdpUrl)) {
launchHttpReachable = true;
break;
}
await waitForManagedLaunchPoll(200, signal);
}
if (!launchHttpReachable) {
signal?.throwIfAborted();
let finalDiagnostic = null;
let diagnosticErrorText = null;
try {
finalDiagnostic = await diagnoseChromeCdp(profile.cdpUrl, 500, 800);
} catch (err) {
diagnosticErrorText = `CDP diagnostic failed: ${safeChromeCdpErrorMessage(err)}.`;
}
signal?.throwIfAborted();
if (diagnosticShowsChromeHttpDiscovery(finalDiagnostic)) launchHttpReachable = true;
const diagnosticText = finalDiagnostic ? formatChromeCdpDiagnostic(finalDiagnostic) : diagnosticErrorText ?? "CDP diagnostic failed.";
if (launchHttpReachable) log.debug(diagnosticText);
else {
const stderrOutput = normalizeOptionalString(stderrDiagnostics.toString()) ?? "";
const stderrSignals = stderrDiagnostics.signals();
const redactedStderrOutput = redactToolPayloadText(stderrOutput);
if (allowSingletonRecovery && stderrSignals.singletonInUse && clearStaleChromeSingletonLocks(userDataDir)) {
log.warn(`Removed stale Chromium Singleton* locks for profile "${profile.name}" and retrying launch.`);
if (!await terminateChromeForRetry(proc, userDataDir)) throw new ManagedChromeCleanupError(`Managed Chrome process ${spawned.pid} survived singleton recovery.`, runningForProcess(proc, spawned.pid));
releaseSpawnAbort();
releaseSpawnAbort = void 0;
return await launchOnceAndWait(false);
}
const stderrHint = redactedStderrOutput ? `\nChrome stderr:\n${sliceUtf16Safe(redactedStderrOutput, -CHROME_STDERR_HINT_MAX_CHARS)}` : "";
const launchHints = chromeLaunchHints({
stderrOutput,
stderrSignals,
resolved,
profile,
launchOptions: headlessOptions
});
try {
proc.kill("SIGKILL");
} catch {}
throw new Error(`Failed to start Chrome CDP on port ${profile.cdpPort} for profile "${profile.name}". ${diagnosticText}${launchHints}${stderrHint}`);
}
}
signal?.throwIfAborted();
const pid = spawned.pid;
log.info(`🦞 openclaw browser started (${exe.kind}) profile "${profile.name}" on 127.0.0.1:${profile.cdpPort} (pid ${pid})`);
return runningForProcess(proc, pid);
} catch (err) {
if (proc) {
const pid = proc.pid;
if (!await signalChromeProcess(proc, "SIGKILL", 5e3) && typeof pid === "number") throw new ManagedChromeCleanupError(`Managed Chrome process ${pid} survived launch cleanup.`, runningForProcess(proc, pid));
}
if (err instanceof ManagedChromeCleanupError) {
if (err.running.proc !== proc) throw err;
throw new Error(`${err.message} Exact child cleanup succeeded on retry.`, { cause: err });
}
throw err;
} finally {
releaseSpawnAbort?.();
proc?.stderr?.off("data", onStderr);
stderrDiagnostics.clear();
}
};
return await launchOnceAndWait(true);
}
function cdpProcessListOwnsBrowser(result, pid) {
if (!result || typeof result !== "object" || !("processInfo" in result)) return false;
const processInfo = result.processInfo;
return Array.isArray(processInfo) && processInfo.some((entry) => entry !== null && typeof entry === "object" && entry.type === "browser" && entry.id === pid);
}
/** Verify that a managed CDP endpoint belongs to the exact spawned browser pid. */
async function isChromeCdpOwnedByPid(cdpUrl, pid, timeoutMs, ssrfPolicy) {
try {
const endpoint = await getChromeWebSocketEndpoint(cdpUrl, timeoutMs, ssrfPolicy);
if (!endpoint) return false;
let owned = false;
await withCdpSocket(endpoint.url, async (send) => {
owned = cdpProcessListOwnsBrowser(await send("SystemInfo.getProcessInfo"), pid);
}, {
commandTimeoutMs: timeoutMs,
handshakeRetries: 0,
handshakeTimeoutMs: timeoutMs,
lookup: endpoint.lookup
});
return owned;
} catch {
return false;
}
}
async function requestGracefulChromeClose(running, timeoutMs, ssrfPolicy, ownsCurrentProcess) {
const commandTimeoutMs = Math.max(1, Math.min(timeoutMs, CHROME_GRACEFUL_CLOSE_COMMAND_TIMEOUT_MS));
let commandSent = false;
try {
const endpoint = await getChromeWebSocketEndpoint(cdpUrlForPort(running.cdpPort), Math.min(commandTimeoutMs, 200), ssrfPolicy);
if (!endpoint) return false;
await withCdpSocket(endpoint.url, async (send) => {
if (!cdpProcessListOwnsBrowser(await send("SystemInfo.getProcessInfo"), running.pid) || ownsCurrentProcess && !ownsCurrentProcess()) return;
commandSent = true;
await send("Browser.close");
}, {
commandTimeoutMs,
handshakeTimeoutMs: commandTimeoutMs,
handshakeRetries: 0,
lookup: endpoint.lookup
});
return commandSent;
} catch (err) {
log.debug(`Chrome graceful close skipped: ${safeChromeCdpErrorMessage(err)}`);
return commandSent;
}
}
/** Stop only the exact managed Chrome owned by this profile across runtimes. */
async function stopOwnedOpenClawChrome(resolved, profile, timeoutMs = CHROME_STOP_TIMEOUT_MS) {
if (!profile.cdpIsLoopback || profile.attachOnly || profile.driver !== "openclaw") return false;
let exe;
try {
exe = resolveBrowserExecutable(resolved, profile);
} catch {
return false;
}
if (!exe) return false;
const userDataDir = resolveOpenClawUserDataDir(profile.name);
const pid = readCurrentHostSingletonPid(userDataDir);
if (pid == null) return false;
const identity = readOwnedManagedChromeIdentity({
pid,
exe,
profile,
userDataDir
});
if (!identity) return false;
if (!(await requestGracefulChromeClose({
pid,
cdpPort: profile.cdpPort
}, timeoutMs, resolved.ssrfPolicy, () => {
const current = readOwnedManagedChromeIdentity({
pid,
exe,
profile,
userDataDir
});
return current !== null && sameManagedChromeIdentity(identity, current);
}) && await waitForPidExit(pid, timeoutMs)) && isPidAlive(pid) && !await terminateOwnedStaleChromeProcess({
identity,
exe,
profile,
userDataDir
}, timeoutMs)) return false;
clearRecoveredChromeSingletonArtifacts(userDataDir, pid);
return true;
}
/** Stop a managed Chrome process and wait for shutdown. */
async function stopOpenClawChrome(running, timeoutMs = CHROME_STOP_TIMEOUT_MS) {
const proc = running.proc;
if (proc.exitCode != null || proc.signalCode != null) return;
if (await requestGracefulChromeClose(running, timeoutMs) && await waitForChromeProcessExit(proc, timeoutMs)) return;
if (await signalChromeProcess(proc, "SIGTERM", timeoutMs)) return;
if (!await signalChromeProcess(proc, "SIGKILL", timeoutMs)) throw new ManagedChromeCleanupError(`Managed Chrome process ${running.pid} survived shutdown.`, running);
}
//#endregion
export { getRoleSnapshotIdentityKeys as A, requiresInspectableBrowserNavigationRedirectsForUrl as B, snapshotAria as C, buildRoleSnapshotFromAriaSnapshot as D, buildRoleSnapshotFromAiSnapshot as E, InvalidBrowserNavigationUrlError as F, readCdpMainFrameDocumentIdentity as H, assertBrowserNavigationAllowed as I, assertBrowserNavigationRedirectChainAllowed as L, CONTENT_ROLES as M, INTERACTIVE_ROLES as N, finalizeRoleSnapshot as O, STRUCTURAL_ROLES as P, assertBrowserNavigationResultAllowed as R, normalizeCdpWsUrl as S, appendRoleSnapshotDepthTruncationMarker as T, waitForCdpCommittedNavigationUrl as U, withBrowserNavigationPolicy as V, resolveBrowserNavigationProxyMode as W, AX_REF_PATTERN as _, isChromeReachable as a, formatAriaSnapshot as b, stopOpenClawChrome as c, parseBrowserMajorVersion as d, readBrowserVersion as f, formatChromeCdpDiagnostic as g, diagnoseChromeCdp as h, isChromeCdpReady as i, parseRoleRef as j, findRoleSnapshotLineRef as k, stopOwnedOpenClawChrome as l, resolveGoogleChromeExecutableForPlatform as m, getChromeWebSocketEndpoint as n, launchOpenClawChrome as o, resolveBrowserExecutableForPlatform as p, isChromeCdpOwnedByPid as r, resolveOpenClawUserDataDir as s, ManagedChromeCleanupError as t, usesOpenClawMockKeychain as u, captureScreenshot as v, snapshotRoleViaCdp as w, getMainFrameDocumentIdentityViaCdp as x, createTargetViaCdp as y, parseBrowserNavigationUrl as z };