UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

1,597 lines 61.6 kB
import { c as normalizeOptionalString } from "./string-coerce-mnp54Vah.js";
import { c as redactSensitiveText, u as redactToolPayloadText } from "./redact-DduLliKq.js";
import { D as resolveIntegerOption } from "./number-coercion-CJQ8TR--.js";
import { t as CONFIG_DIR } from "./utils-CCC-BEJH.js";
import { n as resolvePreferredOpenClawTmpDir } from "./tmp-openclaw-dir-DOKojISm.js";
import { t as createSubsystemLogger } from "./subsystem-BzXSmsuh.js";
import { i as isLoopbackHost } from "./net-DTe7AQiu.js";
import { r as ensurePortAvailable } from "./ports-CoxQQbiY.js";
import { t as normalizeHostname } from "./hostname-D8MH4bbf.js";
import { _ as resolvePinnedHostnameWithPolicy, d as isPrivateNetworkAllowedByPolicy, p as matchesHostnameAllowlist } from "./ssrf-CvPEXMGn.js";
import { r as saveJsonFile, t as loadJsonFile } from "./json-store-CWaMsrLM.js";
import { t as prepareOomScoreAdjustedSpawn } from "./linux-oom-score-eO5nXmjv.js";
import "./number-runtime-DBLVDypr.js";
import "./string-coerce-runtime-CEGJWkQ_.js";
import "./process-runtime-Cl-ggYwo.js";
import { n as redactCdpUrl } from "./browser-config-CB1dJM4Y.js";
import { t as ensureAbsoluteDirectory } from "./sdk-security-runtime-D1sDHmUD.js";
import { D as CHROME_BOOTSTRAP_EXIT_TIMEOUT_MS, J as assertManagedProxyAllowsCdpUrl, M as CHROME_STOP_TIMEOUT_MS, O as CHROME_BOOTSTRAP_PREFS_TIMEOUT_MS, c as isWebSocketUrl, d as withCdpSocket, h as BrowserCdpEndpointBlockedError, i as fetchJson, j as CHROME_STDERR_HINT_MAX_CHARS, k as CHROME_LAUNCH_READY_WINDOW_MS, l as normalizeCdpHttpBaseForJsonEndpoints, n as assertCdpEndpointAllowed, q as DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME, r as fetchCdpChecked, s as isDirectCdpWebSocketEndpoint, t as appendCdpPath, u as openCdpWebSocket, y as BrowserProfileUnavailableError } from "./cdp.helpers-CTQ-1blZ.js";
import { a as DEFAULT_DOWNLOAD_DIR, r as resolveManagedBrowserHeadlessMode, t as getManagedBrowserMissingDisplayError } from "./config-C2rsUqn7.js";
import "./tmp-openclaw-dir-BvAQDjrf.js";
import { r as resolveBrowserExecutableForPlatform } from "./chrome.executables-BGN3WFpZ.js";
import "./subsystem-D319MeC0.js";
import fs from "node:fs";
import path from "node:path";
import fs$1 from "node:fs/promises";
import os from "node:os";
import { spawn } from "node:child_process";
import { isIP } from "node:net";
//#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 = new Set(["http:", "https:"]);
const SAFE_NON_NETWORK_URLS = new Set(["about:blank"]);
function isAllowedNonNetworkNavigationUrl(parsed) {
	return SAFE_NON_NETWORK_URLS.has(parsed.href);
}
function normalizeNavigationUrl(url) {
	return url.trim();
}
/** Raised when a browser navigation URL fails syntax or policy validation. */
var InvalidBrowserNavigationUrlError = class extends Error {
	constructor(message) {
		super(message);
		this.name = "InvalidBrowserNavigationUrlError";
	}
};
/** 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);
	if ((ssrfPolicy?.allowedHostnames ?? []).some((value) => normalizeHostname(value) === normalizedHostname)) return true;
	const hostnameAllowlist = (ssrfPolicy?.hostnameAllowlist ?? []).map((pattern) => normalizeHostname(pattern)).filter(Boolean);
	return hostnameAllowlist.length > 0 ? matchesHostnameAllowlist(normalizedHostname, hostnameAllowlist) : false;
}
/** Assert that a requested browser navigation URL is policy-allowed. */
async function assertBrowserNavigationAllowed(opts) {
	const rawUrl = normalizeNavigationUrl(opts.url);
	if (!rawUrl) throw new InvalidBrowserNavigationUrlError("url is required");
	let parsed;
	try {
		parsed = new URL(rawUrl);
	} catch {
		throw new InvalidBrowserNavigationUrlError(`Invalid URL: ${rawUrl}`);
	}
	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 = normalizeNavigationUrl(opts.url);
	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/browser-proxy-mode.ts
const PROXY_ROUTING_CHROME_ARGS = new Set([
	"--proxy-auto-detect",
	"--proxy-pac-url",
	"--proxy-server"
]);
const PROXY_CONTROL_CHROME_ARGS = 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 && hasExplicitChromeProxyRoutingArg(params.resolved.extraArgs)) return "explicit-browser-proxy";
	return "direct";
}
//#endregion
//#region extensions/browser/src/browser/output-directories.ts
/**
* Browser output directory helper.
*
* Creates absolute output directories while handling macOS system symlink
* aliases such as /tmp and /var safely.
*/
async function resolveSystemDirectoryAlias(dirPath) {
	for (const aliasRoot of ["/tmp", "/var"]) {
		if (dirPath !== aliasRoot && !dirPath.startsWith(`${aliasRoot}${path.sep}`)) continue;
		try {
			if (!(await fs$1.lstat(aliasRoot)).isSymbolicLink()) return dirPath;
			return path.join(await fs$1.realpath(aliasRoot), path.relative(aliasRoot, dirPath));
		} catch {
			return dirPath;
		}
	}
	return dirPath;
}
/** Ensure an absolute browser output directory exists and is safe to use. */
async function ensureOutputDirectory(dirPath) {
	const result = await ensureAbsoluteDirectory(await resolveSystemDirectoryAlias(path.resolve(dirPath)), { scopeLabel: "output directory" });
	if (!result.ok) throw result.error;
}
//#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 = 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 = new Set([
	"article",
	"cell",
	"columnheader",
	"gridcell",
	"heading",
	"listitem",
	"main",
	"navigation",
	"region",
	"rowheader"
]);
/** Structural/container roles — typically skipped in compact mode. */
const STRUCTURAL_ROLES = 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/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.
*/
/** 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 (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");
		let savedVp;
		if (opts.fullPage) {
			const metrics = await send("Page.getLayoutMetrics");
			const size = metrics?.cssContentSize ?? metrics?.contentSize;
			const contentWidth = size?.width ?? 0;
			const contentHeight = size?.height ?? 0;
			if (contentWidth > 0 && contentHeight > 0) {
				const v = (await send("Runtime.evaluate", {
					expression: "({ w: window.innerWidth, h: window.innerHeight, dpr: window.devicePixelRatio, sw: screen.width, sh: screen.height })",
					returnByValue: true
				}))?.result?.value;
				const currentW = v?.w ?? 0;
				const currentH = v?.h ?? 0;
				savedVp = {
					w: currentW,
					h: currentH,
					dpr: v?.dpr ?? 1,
					sw: v?.sw ?? currentW,
					sh: v?.sh ?? currentH
				};
				await send("Emulation.setDeviceMetricsOverride", {
					width: Math.ceil(Math.max(currentW, contentWidth)),
					height: Math.ceil(Math.max(currentH, contentHeight)),
					deviceScaleFactor: savedVp.dpr,
					mobile: false,
					screenWidth: savedVp.sw,
					screenHeight: savedVp.sh
				});
			}
		}
		const format = opts.format ?? "png";
		const quality = format === "jpeg" ? Math.max(0, Math.min(100, Math.round(opts.quality ?? 85))) : void 0;
		try {
			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");
		} finally {
			if (savedVp) {
				await send("Emulation.clearDeviceMetricsOverride").catch(() => {});
				try {
					const p = (await send("Runtime.evaluate", {
						expression: "({ w: window.innerWidth, h: window.innerHeight, dpr: window.devicePixelRatio })",
						returnByValue: true
					}))?.result?.value;
					if (p?.w !== savedVp.w || p?.h !== savedVp.h || p?.dpr !== savedVp.dpr) await send("Emulation.setDeviceMetricsOverride", {
						width: savedVp.w,
						height: savedVp.h,
						deviceScaleFactor: savedVp.dpr,
						mobile: false,
						screenWidth: savedVp.sw,
						screenHeight: savedVp.sh
					});
				} catch {}
			}
		}
	}, { commandTimeoutMs: opts.timeoutMs });
}
/** Create a new browser target after applying navigation and CDP SSRF policy. */
async function createTargetViaCdp(opts) {
	await assertBrowserNavigationAllowed({
		url: opts.url,
		...withBrowserNavigationPolicy(opts.ssrfPolicy)
	});
	let wsUrl;
	if (isDirectCdpWebSocketEndpoint(opts.cdpUrl)) {
		await assertCdpEndpointAllowed(opts.cdpUrl, opts.ssrfPolicy);
		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, void 0, opts.ssrfPolicy);
		} 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 {
		await assertCdpEndpointAllowed(candidateWsUrl, opts.ssrfPolicy);
		return await withCdpSocket(candidateWsUrl, async (send) => {
			const targetId = (await send("Target.createTarget", { url: opts.url }))?.targetId?.trim() ?? "";
			if (!targetId) throw new Error("CDP Target.createTarget returned no targetId");
			await prepareCdpTargetSession(send, targetId);
			return { targetId };
		}, {
			commandTimeoutMs: opts.timeouts?.httpTimeoutMs ?? 5e3,
			handshakeTimeoutMs: opts.timeouts?.handshakeTimeoutMs
		});
	} catch (err) {
		lastError = err;
	}
	if (lastError instanceof Error) throw lastError;
	throw new Error("CDP Target.createTarget failed");
}
async function prepareCdpTargetSession(send, targetId) {
	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);
	} finally {
		await send("Target.detachFromTarget", { sessionId }).catch(() => {});
	}
}
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(() => {});
}
const AX_REF_PATTERN = new RegExp(`^ax\\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${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 ?? []).filter((c) => byId.has(c));
		for (let i = children.length - 1; i >= 0; i--) {
			const child = children[i];
			/* c8 ignore next 3 */
			if (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 });
}
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);
		tree[childIndex].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;
		tree[current.index].depth = current.depth;
		for (const child of (tree[current.index]?.children ?? []).toReversed()) 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.maxDepth !== void 0 && node.depth > options.maxDepth) return false;
	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, indentOffset = 0) {
	const node = tree[index];
	if (!node) return;
	if (shouldIncludeRoleNode(node, options)) {
		const indent = "  ".repeat(Math.max(0, node.depth + indentOffset));
		const name = node.name ? ` "${node.name.replaceAll("\"", "\\\"")}"` : "";
		const ref = node.ref ? ` [ref=${node.ref}]` : "";
		const nth = node.nth !== void 0 && node.nth > 0 ? ` [nth=${node.nth}]` : "";
		const value = node.value ? ` value="${node.value.replaceAll("\"", "\\\"")}"` : "";
		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, 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, 100),
            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 : [];
	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();
	await Promise.all(Object.values(refs).map(async (ref) => {
		if (ref.role !== "link" || !ref.backendDOMNodeId) return;
		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();
	await Promise.all(tree.map(async (node) => {
		if (node.role.toLowerCase() !== "iframe" || !node.backendDOMNodeId) return;
		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 refsByKey = /* @__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;
		refsByKey.set(key, [...refsByKey.get(key) ?? [], ref]);
		refs[ref] = {
			role,
			...node.name ? { name: node.name } : {},
			...nth > 0 ? { nth } : {},
			...node.backendDOMNodeId ? { backendDOMNodeId: node.backendDOMNodeId } : {},
			...params.frameId ? { frameId: params.frameId } : {}
		};
	}
	for (const refList of refsByKey.values()) {
		if (refList.length > 1) continue;
		const ref = refList[0];
		if (ref) {
			delete refs[ref]?.nth;
			const node = tree.find((entry) => entry.ref === ref);
			if (node) delete node.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]) refs[node.ref].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 = [];
	for (const root of roots) renderRoleTree(tree, root, lines, params.options);
	if (params.recurseIframes) {
		const iframeNodes = tree.filter((node) => node.ref && node.frameId);
		for (const iframe of iframeNodes) {
			const marker = `[ref=${iframe.ref}]`;
			const lineIndex = lines.findIndex((line) => line.includes(marker));
			if (lineIndex < 0 || !iframe.frameId) continue;
			const child = await buildCdpRoleSnapshot({
				...params,
				frameId: iframe.frameId,
				recurseIframes: false
			}).catch(() => null);
			if (!child?.lines.length) continue;
			Object.assign(refs, child.refs);
			lines.splice(lineIndex + 1, 0, ...child.lines.map((line) => `  ${line}`));
		}
	}
	const refValues = Object.values(refs);
	return {
		lines,
		refs,
		stats: {
			refs: refValues.length,
			interactive: refValues.filter((ref) => INTERACTIVE_ROLES.has(ref.role)).length
		}
	};
}
/** 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: true,
			nextRef: { value: 1 }
		});
		const snapshot = built.lines.join("\n").trim() || (opts.options?.interactive ? "(no interactive elements)" : "(empty page)");
		return {
			snapshot,
			refs: built.refs,
			stats: {
				lines: snapshot.split("\n").length,
				chars: snapshot.length,
				refs: built.stats.refs,
				interactive: built.stats.interactive
			}
		};
	}, { commandTimeoutMs: opts.timeoutMs ?? 5e3 });
}
//#endregion
//#region extensions/browser/src/infra/ws.ts
/**
* WebSocket payload normalization helpers for Browser gateway transports.
*/
/** Converts raw WebSocket payload shapes into UTF-8 strings. */
function rawDataToString(data) {
	if (typeof data === "string") return data;
	if (Buffer.isBuffer(data)) return data.toString("utf8");
	if (Array.isArray(data)) return Buffer.concat(data).toString("utf8");
	if (ArrayBuffer.isView(data)) return Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString("utf8");
	if (data instanceof ArrayBuffer) return Buffer.from(data).toString("utf8");
	return String(data);
}
//#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) {
	const ctrl = new AbortController();
	const t = setTimeout(ctrl.abort.bind(ctrl), timeoutMs);
	try {
		const { response, release } = await fetchCdpChecked(appendCdpPath(cdpUrl, "/json/version"), timeoutMs, { signal: ctrl.signal }, ssrfPolicy);
		try {
			const data = await response.json();
			if (!data || typeof data !== "object") throw new Error("CDP /json/version returned non-object JSON");
			return data;
		} finally {
			await release();
		}
	} finally {
		clearTimeout(t);
	}
}
async function diagnoseCdpHealthCommand(wsUrl, timeoutMs = 800) {
	return await new Promise((resolve) => {
		const ws = openCdpWebSocket(wsUrl, { handshakeTimeoutMs: timeoutMs });
		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 });
				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);
			try {
				ws.close();
			} catch {}
			resolve(value);
		};
		const timer = setTimeout(() => {
			try {
				ws.terminate();
			} catch {}
			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("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) ? " In WSL2-to-Windows Chrome setups, this can be a stale netsh portproxy self-loop where svchost/iphlpsvc owns the CDP port instead of chrome.exe; verify with tasklist /svc and curl /json/version, then remove any 127.0.0.1:9222 -> 127.0.0.1:9222 portproxy rule." : "";
	return `CDP diagnostic: ${diagnostic.code} after ${diagnostic.elapsedMs}ms; cdp=${redactedCdpUrl}${websocket}; ${diagnostic.message}.${wslPortproxyHint}`;
}
function isLikelyEmptyHttpReply(message) {
	return /empty reply|other side closed|socket closed|terminated before response/i.test(message);
}
async function diagnoseCdpWebSocketEndpoint(params) {
	const health = await diagnoseCdpHealthCommand(params.wsUrl, params.handshakeTimeoutMs);
	if (!health.ok) return failureDiagnostic({
		cdpUrl: params.cdpUrl,
		wsUrl: params.wsUrl,
		code: health.code,
		message: health.message,
		startedAt: params.startedAt
	});
	if (params.version) return {
		ok: true,
		cdpUrl: params.cdpUrl,
		wsUrl: params.wsUrl,
		browser: params.version.Browser,
		userAgent: params.version["User-Agent"],
		elapsedMs: elapsedSince(params.startedAt)
	};
	return {
		ok: true,
		cdpUrl: params.cdpUrl,
		wsUrl: params.wsUrl,
		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();
	try {
		await assertCdpEndpointAllowed(cdpUrl, ssrfPolicy);
	} catch (err) {
		return failureDiagnostic({
			cdpUrl,
			code: "ssrf_blocked",
			message: safeChromeCdpErrorMessage(err),
			startedAt
		});
	}
	if (isDirectCdpWebSocketEndpoint(cdpUrl)) return await diagnoseCdpWebSocketEndpoint({
		cdpUrl,
		wsUrl: cdpUrl,
		startedAt,
		handshakeTimeoutMs
	});
	const discoveryUrl = isWebSocketUrl(cdpUrl) ? normalizeCdpHttpBaseForJsonEndpoints(cdpUrl) : cdpUrl;
	let version;
	try {
		version = await readChromeVersion(discoveryUrl, timeoutMs, ssrfPolicy);
	} catch (err) {
		if (isWebSocketUrl(cdpUrl)) return await diagnoseCdpWebSocketEndpoint({
			cdpUrl,
			wsUrl: cdpUrl,
			startedAt,
			handshakeTimeoutMs
		});
		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,
			version
		});
		return failureDiagnostic({
			cdpUrl,
			code: "missing_websocket_debugger_url",
			message: "CDP /json/version did not include webSocketDebuggerUrl",
			startedAt
		});
	}
	const wsUrl = normalizeCdpWsUrl(wsUrlRaw, discoveryUrl);
	try {
		await assertCdpEndpointAllowed(wsUrl, ssrfPolicy);
	} catch (err) {
		return failureDiagnostic({
			cdpUrl,
			wsUrl,
			code: "websocket_ssrf_blocked",
			message: safeChromeCdpErrorMessage(err),
			startedAt
		});
	}
	const health = await diagnoseCdpHealthCommand(wsUrl, handshakeTimeoutMs);
	if (!health.ok) {
		if (isWebSocketUrl(cdpUrl) && wsUrl !== cdpUrl) {
			if ((await diagnoseCdpHealthCommand(cdpUrl, handshakeTimeoutMs)).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.profile-decoration.ts
/**
* OpenClaw-managed Chrome profile decoration.
*
* Applies a stable profile name, color, download directory, and clean-exit
* markers to the managed Chrome profile's Local State and Preferences files.
*/
function decoratedMarkerPath(userDataDir) {
	return path.join(userDataDir, ".openclaw-profile-decorated");
}
function safeReadJson(filePath) {
	const parsed = loadJsonFile(filePath);
	return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : null;
}
function safeWriteJson(filePath, data) {
	saveJsonFile(filePath, data);
}
function asRecord(value) {
	return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
}
function readNestedRecord(root, key) {
	return asRecord(asRecord(root)?.[key]);
}
function setDeep(obj, keys, value) {
	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];
	}
	node[keys[keys.length - 1] ?? ""] = 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 profile = safeReadJson(localStatePath)?.profile;
	const info = readNestedRecord(readNestedRecord(profile, "info_cache"), "Default");
	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;
}
/**
* 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);
	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_HTTP_DISCOVERY_FAILURE_CODES = new Set([
	"ssrf_blocked",
	"http_unreachable",
	"http_status_failed",
	"invalid_json"
]);
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 processExists(pid) {
	if (!Number.isInteger(pid) || pid <= 0) return false;
	try {
		process.kill(pid, 0);
		return true;
	} catch (err) {
		if (err.code === "EPERM") return true;
		return false;
	}
}
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 lockPath = path.join(userDataDir, "SingletonLock");
	let target;
	try {
		target = fs.readlinkSync(lockPath);
	} catch {
		return false;
	}
	const match = /^(?<lockHost>.+)-(?<pid>\d+)$/.exec(target);
	if (!match?.groups) return false;
	const lockHost = normalizeOptionalString(match.groups.lockHost) ?? "";
	const pid = Number.parseInt(match.groups.pid ?? "", 10);
	if (lockHost === hostname && processExists(pid)) return false;
	clearChromeSingletonArtifacts(userDataDir);
	return true;
}
async function waitForChromeProcessExit(proc, timeoutMs) {
	if (proc.exitCode != null || proc.signalCode != null || proc.killed) return;
	await new Promise((resolve) => {
		const timer = setTimeout(() => {
			proc.off("exit", onExit);
			proc.off("close", onExit);
			resolve();
		}, timeoutMs);
		const onExit = () => {
			clearTimeout(timer);
			resolve();
		};
		proc.once("exit", onExit);
		proc.once("close", onExit);
	});
}
async function terminateChromeForRetry(proc, userDataDir) {
	try {
		proc.kill("SIGKILL");
	} catch {}
	await waitForChromeProcessExit(proc, CHROME_BOOTSTRAP_EXIT_TIMEOUT_MS);
	clearStaleChromeSingletonLocks(userDataDir);
}
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 (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 (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: ")}` : "";
}
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 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 (headlessMode.headless) {
		args.push("--headless=new");
		args.push("--disable-gpu");
	}
	if (resolved.noSandbox) args.push("--no-sandbox");
	if (process.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) {
	return new Promise((resolve) => {
		const ws = openCdpWebSocket(url, { handshakeTimeoutMs: timeoutMs });
		let settled = false;
		const finish = (value) => {
			if (settled) return;
			settled = true;
			resolve(value);
		};
		ws.once("open", () => {
			try {
				ws.close();
			} catch {}
			finish(true);
		});
		ws.once("error", () => finish(false));
		ws.once("close", () => finish(false));
	});
}
/** Return true when a Chrome CDP endpoint is reachable over HTTP. */
async function isChromeReachable(cdpUrl, timeoutMs = 500, ssrfPolicy) {
	try {
		await assertCdpEndpointAllowed(cdpUrl, ssrfPolicy);
		if (isDirectCdpWebSocketEndpoint(cdpUrl)) return await canOpenWebSocket(cdpUrl, timeoutMs);
		if (await fetchChromeVersion(isWebSocketUrl(cdpUrl) ? normalizeCdpHttpBaseForJsonEndpoints(cdpUrl) : cdpUrl, timeoutMs, ssrfPolicy)) return true;
		if (isWebSocketUrl(cdpUrl)) return await canOpenWebSocket(cdpUrl, timeoutMs);
		return false;
	} catch {
		return false;
	}
}
async function fetchChromeVersion(cdpUrl, timeoutMs = 500, ssrfPolicy) {
	try {
		return await readChromeVersion(cdpUrl, timeoutMs, ssrfPolicy);
	} catch {
		return null;
	}
}
/** Resolve a usable Chrome DevTools WebSocket URL from a CDP endpoint. */
async function getChromeWebSocketUrl(cdpUrl, timeoutMs = 500, ssrfPolicy) {
	await assertCdpEndpointAllowed(cdpUrl, ssrfPolicy);
	if (isDirectCdpWebSocketEndpoint(cdpUrl)) return cdpUrl;
	const discoveryUrl = isWebSocketUrl(cdpUrl) ? normalizeCdpHttpBaseForJsonEndpoints(cdpUrl) : cdpUrl;
	const wsUrl = normalizeOptionalString((await fetchChromeVersion(discoveryUrl, timeoutMs, ssrfPolicy))?.webSocketDebuggerUrl) ?? "";
	if (!wsUrl) {
		if (isWebSocketUrl(cdpUrl)) return cdpUrl;
		return null;
	}
	const normalizedWsUrl = normalizeCdpWsUrl(wsUrl, discoveryUrl);
	await assertCdpEndpointAllowed(normalizedWsUrl, ssrfPolicy);
	return normalizedWsUrl;
}
/** 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;
}
/** Launch or attach to the managed OpenClaw Chrome profile. */
async function launchOpenClawChrome(resolved, profile, launchOptions = {}) {
	if (!profile.cdpIsLoopback) throw new Error(`Profile "${profile.name}" is remote; cannot launch local Chrome.`);
	const headlessMode = resolveManagedBrowserHeadlessMode(resolved, profile, launchOptions);
	const missingDisplayError = getManagedBrowserMissingDisplayError(resolved, profile, launchOptions);
	if (missingDisplayError) throw new BrowserProfileUnavailableError(missingDisplayError);
	try {
		assertManagedProxyAllowsCdpUrl(profile.cdpUrl);
	} catch (err) {
		throw new BrowserProfileUnavailableError(`Browser profile "${profile.name}" cannot launch: ${err instanceof Error ? err.message : String(err)}`);
	}
	await ensurePortAvailable(profile.cdpPort);
	const exe = resolveBrowserExecutable(resolved, profile);
	if (!exe) throw new Error("No supported browser found (Chrome/Brave/Edge/Chromium on macOS, Linux, or Windows).");
	const userDataDir = resolveOpenClawUserDataDir(profile.name);
	fs.mkdirSync(userDataDir, { recursive: true });
	await ensureOutputDirectory(DEFAULT_DOWNLOAD_DIR);
	const needsDecorate = !isProfileDecorated(userDataDir, profile.name, (profile.color ?? "#FF4500").toUpperCase(), DEFAULT_DOWNLOAD_DIR);
	const spawnOnce = () => {
		const args = buildOpenClawChromeLaunchArgs({
			resolved,
			profile,
			userDataDir,
			...launchOptions
		});
		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 });
		return spawn(preparedSpawn.command, preparedSpawn.args, {
			stdio: [
				"ignore",
				"ignore",
				"pipe"
			],
			env: preparedSpawn.env
		});
	};
	const startedAt = Date.now();
	const localStatePath = path.join(userDataDir, "Local State");
	const preferencesPath = path.join(userDataDir, "Default", "Preferences");
	if (!exists(localStatePath) || !exists(preferencesPath)) {
		const bootstrap = spawnOnce();
		const deadline = Date.now() + CHROME_BOOTSTRAP_PREFS_TIMEOUT_MS;
		while (Date.now() < deadline) {
			if (exists(localStatePath) && exists(preferencesPath)) break;
			await new Promise((r) => {
				setTimeout(r, 100);
			});
		}
		try {
			bootstrap.kill("SIGTERM");
		} catch {}
		const exitDeadline = Date.now() + CHROME_BOOTSTRAP_EXIT_TIMEOUT_MS;
		while (Date.now() < exitDeadline) {
			if (bootstrap.exitCode != null) break;
			await new Promise((r) => {
				setTimeout(r, 50);
			});
		}
	}
	if (needsDecorate) try {
		decorateOpenClawProfile(userDataDir, {
			name: profile.name,
			color: profile.color,
			downloadDir: DEFAULT_DOWNLOAD_DIR
		});
		log.info(`🦞 openclaw browser profile decorated (${profile.color})`);
	} catch (err) {
		log.warn(`openclaw browser profile decoration failed: ${String(err)}`);
	}
	try {
		ensureProfileCleanExit(userDataDir);
	} catch (err) {
		log.warn(`openclaw browser clean-exit prefs failed: ${String(err)}`);
	}
	const launchOnceAndWait = async (allowSingletonRecovery) => {
		const proc = spawnOnce();
		const stderrChunks = [];
		const onStderr = (chunk) => {
			stderrChunks.push(chunk);
		};
		proc.stderr?.on("data", onStderr);
		try {
			const readyDeadline = Date.now() + (resolved.localLaunchTimeoutMs ?? CHROME_LAUNCH_READY_WINDOW_MS);
			let launchHttpReachable = false;
			while (Date.now() < readyDeadline) {
				if (await isChromeReachable(profile.cdpUrl)) {
					launchHttpReachable = true;
					break;
				}
				await new Promise((r) => {
					setTimeout(r, 200);
				});
			}
			if (!launchHttpReachable) {
				let finalDiagnostic = null;
				let diagnosticErrorText = null;
				try {
					finalDiagnostic = await diagnoseChromeCdp(profile.cdpUrl, 500, 800);
				} catch (err) {
					diagnosticErrorText = `CDP diagnostic failed: ${safeChromeCdpErrorMessage(err)}.`;
				}
				if (diagnosticShowsChromeHttpDiscovery(finalDiagnostic)) launchHttpReachable = true;
				const diagnosticText = finalDiagnostic ? formatChromeCdpDiagnostic(finalDiagnostic) : diagnosticErrorText ?? "CDP diagnostic failed.";
				if (launchHttpReachable) log.debug(diagnosticText);
				else {
					const stderrOutput = normalizeOptionalString(Buffer.concat(stderrChunks).toString("utf8")) ?? "";
					const redactedStderrOutput = redactToolPayloadText(stderrOutput);
					if (allowSingletonRecovery && CHROME_SINGLETON_IN_USE_PATTERN.test(stderrOutput) && clearStaleChromeSingletonLocks(userDataDir)) {
						log.warn(`Removed stale Chromium Singleton* locks for profile "${profile.name}" and retrying launch.`);
						await terminateChromeForRetry(proc, userDataDir);
						return await launchOnceAndWait(false);
					}
					const stderrHint = redactedStderrOutput ? `\nChrome stderr:\n${redactedStderrOutput.slice(0, CHROME_STDERR_HINT_MAX_CHARS)}` : "";
					const launchHints = chromeLaunchHints({
						stderrOutput,
						resolved,
						profile,
						launchOptions
					});
					try {
						proc.kill("SIGKILL");
					} catch {}
					throw new Error(`Failed to start Chrome CDP on port ${profile.cdpPort} for profile "${profile.name}". ${diagnosticText}${launchHints}${stderrHint}`);
				}
			}
			const pid = proc.pid ?? -1;
			log.info(`🦞 openclaw browser started (${exe.kind}) profile "${profile.name}" on 127.0.0.1:${profile.cdpPort} (pid ${pid})`);
			return {
				pid,
				exe,
				userDataDir,
				cdpPort: profile.cdpPort,
				startedAt,
				proc,
				headless: headlessMode.headless,
				headlessSource: headlessMode.source
			};
		} finally {
			proc.stderr?.off("data", onStderr);
			stderrChunks.length = 0;
		}
	};
	return await launchOnceAndWait(true);
}
/** Stop a managed Chrome process and wait for shutdown. */
async function stopOpenClawChrome(running, timeoutMs = CHROME_STOP_TIMEOUT_MS) {
	const proc = running.proc;
	try {
		if (proc.killed) return;
		try {
			proc.kill("SIGTERM");
		} catch {}
		const start = Date.now();
		while (Date.now() - start < timeoutMs) {
			if (!proc.exitCode && proc.killed) break;
			if (!await isChromeReachable(cdpUrlForPort(running.cdpPort), 200)) return;
			const remainingMs = timeoutMs - (Date.now() - start);
			await new Promise((r) => {
				setTimeout(r, Math.max(1, Math.min(100, remainingMs)));
			});
		}
		try {
			proc.kill("SIGKILL");
		} catch {}
	} finally {
		const release = running.releaseCdpProxyBypass;
		if (release) {
			running.releaseCdpProxyBypass = void 0;
			try {
				release();
			} catch {}
		}
	}
}
//#endregion
export { assertBrowserNavigationRedirectChainAllowed as C, withBrowserNavigationPolicy as E, assertBrowserNavigationAllowed as S, requiresInspectableBrowserNavigationRedirectsForUrl as T, INTERACTIVE_ROLES as _, resolveOpenClawUserDataDir as a, resolveBrowserNavigationProxyMode as b, formatChromeCdpDiagnostic as c, createTargetViaCdp as d, formatAriaSnapshot as f, CONTENT_ROLES as g, snapshotRoleViaCdp as h, launchOpenClawChrome as i, AX_REF_PATTERN as l, snapshotAria as m, isChromeCdpReady as n, stopOpenClawChrome as o, normalizeCdpWsUrl as p, isChromeReachable as r, diagnoseChromeCdp as s, getChromeWebSocketUrl as t, captureScreenshot as u, STRUCTURAL_ROLES as v, assertBrowserNavigationResultAllowed as w, InvalidBrowserNavigationUrlError as x, ensureOutputDirectory as y };