@gguf/claw
Version:
Multi-channel AI gateway with extensible messaging integrations
1,593 lines (1,581 loc) • 56.9 kB
JavaScript
import { b as CONFIG_DIR } from "./registry-dD2_jBuv.js";
import { t as createSubsystemLogger } from "./subsystem-CGx2ESmP.js";
import { n as loadConfig } from "./config-42hNXHap.js";
import { i as isErrno } from "./errors-BxR4oB-s.js";
import { a as resolvePinnedHostnameWithPolicy } from "./ssrf-DH-Ltk62.js";
import path from "node:path";
import fs from "node:fs";
import os from "node:os";
import fs$1 from "node:fs/promises";
import { execFileSync, spawn } from "node:child_process";
import net from "node:net";
import { createServer } from "node:http";
import WebSocket$1, { WebSocketServer } from "ws";
import { Buffer as Buffer$1 } from "node:buffer";
//#region src/browser/constants.ts
const DEFAULT_OPENCLAW_BROWSER_ENABLED = true;
const DEFAULT_BROWSER_EVALUATE_ENABLED = true;
const DEFAULT_OPENCLAW_BROWSER_COLOR = "#FF4500";
const DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME = "openclaw";
const DEFAULT_BROWSER_DEFAULT_PROFILE_NAME = "chrome";
const DEFAULT_AI_SNAPSHOT_MAX_CHARS = 8e4;
const DEFAULT_AI_SNAPSHOT_EFFICIENT_MAX_CHARS = 1e4;
const DEFAULT_AI_SNAPSHOT_EFFICIENT_DEPTH = 6;
//#endregion
//#region src/gateway/net.ts
function isLoopbackAddress(ip) {
if (!ip) return false;
if (ip === "127.0.0.1") return true;
if (ip.startsWith("127.")) return true;
if (ip === "::1") return true;
if (ip.startsWith("::ffff:127.")) return true;
return false;
}
/**
* Check if a hostname or IP refers to the local machine.
* Handles: localhost, 127.x.x.x, ::1, [::1], ::ffff:127.x.x.x
* Note: 0.0.0.0 and :: are NOT loopback - they bind to all interfaces.
*/
function isLoopbackHost(host) {
if (!host) return false;
const h = host.trim().toLowerCase();
if (h === "localhost") return true;
return isLoopbackAddress(h.startsWith("[") && h.endsWith("]") ? h.slice(1, -1) : h);
}
/**
* Security check for WebSocket URLs (CWE-319: Cleartext Transmission of Sensitive Information).
*
* Returns true if the URL is secure for transmitting data:
* - wss:// (TLS) is always secure
* - ws:// is only secure for loopback addresses (localhost, 127.x.x.x, ::1)
*
* All other ws:// URLs are considered insecure because both credentials
* AND chat/conversation data would be exposed to network interception.
*/
function isSecureWebSocketUrl(url) {
let parsed;
try {
parsed = new URL(url);
} catch {
return false;
}
if (parsed.protocol === "wss:") return true;
if (parsed.protocol !== "ws:") return false;
return isLoopbackHost(parsed.hostname);
}
//#endregion
//#region src/infra/ws.ts
function rawDataToString(data, encoding = "utf8") {
if (typeof data === "string") return data;
if (Buffer$1.isBuffer(data)) return data.toString(encoding);
if (Array.isArray(data)) return Buffer$1.concat(data).toString(encoding);
if (data instanceof ArrayBuffer) return Buffer$1.from(data).toString(encoding);
return Buffer$1.from(String(data)).toString(encoding);
}
//#endregion
//#region src/browser/extension-relay.ts
const RELAY_AUTH_HEADER = "x-openclaw-relay-token";
function headerValue(value) {
if (!value) return;
if (Array.isArray(value)) return value[0];
return value;
}
function getHeader(req, name) {
return headerValue(req.headers[name.toLowerCase()]);
}
function getRelayAuthTokenFromRequest(req, url) {
const headerToken = getHeader(req, RELAY_AUTH_HEADER)?.trim();
if (headerToken) return headerToken;
const queryToken = url?.searchParams.get("token")?.trim();
if (queryToken) return queryToken;
}
function parseBaseUrl(raw) {
const parsed = new URL(raw.trim().replace(/\/$/, ""));
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error(`extension relay cdpUrl must be http(s), got ${parsed.protocol}`);
const host = parsed.hostname;
const port = parsed.port?.trim() !== "" ? Number(parsed.port) : parsed.protocol === "https:" ? 443 : 80;
if (!Number.isFinite(port) || port <= 0 || port > 65535) throw new Error(`extension relay cdpUrl has invalid port: ${parsed.port || "(empty)"}`);
return {
host,
port,
baseUrl: parsed.toString().replace(/\/$/, "")
};
}
function text(res, status, bodyText) {
const body = Buffer.from(bodyText);
res.write(`HTTP/1.1 ${status} ${status === 200 ? "OK" : "ERR"}\r\nContent-Type: text/plain; charset=utf-8\r
Content-Length: ${body.length}\r\nConnection: close\r
\r
`);
res.write(body);
res.end();
}
function rejectUpgrade(socket, status, bodyText) {
text(socket, status, bodyText);
try {
socket.destroy();
} catch {}
}
const serversByPort = /* @__PURE__ */ new Map();
function resolveGatewayAuthToken() {
const envToken = process.env.OPENCLAW_GATEWAY_TOKEN?.trim() || process.env.CLAWDBOT_GATEWAY_TOKEN?.trim();
if (envToken) return envToken;
try {
const configToken = loadConfig().gateway?.auth?.token?.trim();
if (configToken) return configToken;
} catch {}
return null;
}
function resolveRelayAuthToken() {
const gatewayToken = resolveGatewayAuthToken();
if (gatewayToken) return gatewayToken;
throw new Error("extension relay requires gateway auth token (set gateway.auth.token or OPENCLAW_GATEWAY_TOKEN)");
}
function isAddrInUseError(err) {
return typeof err === "object" && err !== null && "code" in err && err.code === "EADDRINUSE";
}
async function looksLikeOpenClawRelay(baseUrl) {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), 500);
try {
const statusUrl = new URL("/extension/status", `${baseUrl}/`).toString();
const res = await fetch(statusUrl, { signal: ctrl.signal });
if (!res.ok) return false;
return typeof (await res.json()).connected === "boolean";
} catch {
return false;
} finally {
clearTimeout(timer);
}
}
function relayAuthTokenForUrl(url) {
try {
if (!isLoopbackHost(new URL(url).hostname)) return null;
return resolveGatewayAuthToken();
} catch {
return null;
}
}
function getChromeExtensionRelayAuthHeaders(url) {
const token = relayAuthTokenForUrl(url);
if (!token) return {};
return { [RELAY_AUTH_HEADER]: token };
}
async function ensureChromeExtensionRelayServer(opts) {
const info = parseBaseUrl(opts.cdpUrl);
if (!isLoopbackHost(info.host)) throw new Error(`extension relay requires loopback cdpUrl host (got ${info.host})`);
const existing = serversByPort.get(info.port);
if (existing) return existing;
const relayAuthToken = resolveRelayAuthToken();
let extensionWs = null;
const cdpClients = /* @__PURE__ */ new Set();
const connectedTargets = /* @__PURE__ */ new Map();
const pendingExtension = /* @__PURE__ */ new Map();
let nextExtensionId = 1;
const sendToExtension = async (payload) => {
const ws = extensionWs;
if (!ws || ws.readyState !== WebSocket$1.OPEN) throw new Error("Chrome extension not connected");
ws.send(JSON.stringify(payload));
return await new Promise((resolve, reject) => {
const timer = setTimeout(() => {
pendingExtension.delete(payload.id);
reject(/* @__PURE__ */ new Error(`extension request timeout: ${payload.params.method}`));
}, 3e4);
pendingExtension.set(payload.id, {
resolve,
reject,
timer
});
});
};
const broadcastToCdpClients = (evt) => {
const msg = JSON.stringify(evt);
for (const ws of cdpClients) {
if (ws.readyState !== WebSocket$1.OPEN) continue;
ws.send(msg);
}
};
const sendResponseToCdp = (ws, res) => {
if (ws.readyState !== WebSocket$1.OPEN) return;
ws.send(JSON.stringify(res));
};
const ensureTargetEventsForClient = (ws, mode) => {
for (const target of connectedTargets.values()) if (mode === "autoAttach") ws.send(JSON.stringify({
method: "Target.attachedToTarget",
params: {
sessionId: target.sessionId,
targetInfo: {
...target.targetInfo,
attached: true
},
waitingForDebugger: false
}
}));
else ws.send(JSON.stringify({
method: "Target.targetCreated",
params: { targetInfo: {
...target.targetInfo,
attached: true
} }
}));
};
const routeCdpCommand = async (cmd) => {
switch (cmd.method) {
case "Browser.getVersion": return {
protocolVersion: "1.3",
product: "Chrome/OpenClaw-Extension-Relay",
revision: "0",
userAgent: "OpenClaw-Extension-Relay",
jsVersion: "V8"
};
case "Browser.setDownloadBehavior": return {};
case "Target.setAutoAttach":
case "Target.setDiscoverTargets": return {};
case "Target.getTargets": return { targetInfos: Array.from(connectedTargets.values()).map((t) => ({
...t.targetInfo,
attached: true
})) };
case "Target.getTargetInfo": {
const params = cmd.params ?? {};
const targetId = typeof params.targetId === "string" ? params.targetId : void 0;
if (targetId) {
for (const t of connectedTargets.values()) if (t.targetId === targetId) return { targetInfo: t.targetInfo };
}
if (cmd.sessionId && connectedTargets.has(cmd.sessionId)) {
const t = connectedTargets.get(cmd.sessionId);
if (t) return { targetInfo: t.targetInfo };
}
return { targetInfo: Array.from(connectedTargets.values())[0]?.targetInfo };
}
case "Target.attachToTarget": {
const params = cmd.params ?? {};
const targetId = typeof params.targetId === "string" ? params.targetId : void 0;
if (!targetId) throw new Error("targetId required");
for (const t of connectedTargets.values()) if (t.targetId === targetId) return { sessionId: t.sessionId };
throw new Error("target not found");
}
default: return await sendToExtension({
id: nextExtensionId++,
method: "forwardCDPCommand",
params: {
method: cmd.method,
sessionId: cmd.sessionId,
params: cmd.params
}
});
}
};
const server = createServer((req, res) => {
const path = new URL(req.url ?? "/", info.baseUrl).pathname;
if (path.startsWith("/json")) {
const token = getHeader(req, RELAY_AUTH_HEADER);
if (!token || token !== relayAuthToken) {
res.writeHead(401);
res.end("Unauthorized");
return;
}
}
if (req.method === "HEAD" && path === "/") {
res.writeHead(200);
res.end();
return;
}
if (path === "/") {
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
res.end("OK");
return;
}
if (path === "/extension/status") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ connected: Boolean(extensionWs) }));
return;
}
const cdpWsUrl = `${`ws://${req.headers.host?.trim() || `${info.host}:${info.port}`}`}/cdp`;
if ((path === "/json/version" || path === "/json/version/") && (req.method === "GET" || req.method === "PUT")) {
const payload = {
Browser: "OpenClaw/extension-relay",
"Protocol-Version": "1.3"
};
if (extensionWs) payload.webSocketDebuggerUrl = cdpWsUrl;
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(payload));
return;
}
if (new Set([
"/json",
"/json/",
"/json/list",
"/json/list/"
]).has(path) && (req.method === "GET" || req.method === "PUT")) {
const list = Array.from(connectedTargets.values()).map((t) => ({
id: t.targetId,
type: t.targetInfo.type ?? "page",
title: t.targetInfo.title ?? "",
description: t.targetInfo.title ?? "",
url: t.targetInfo.url ?? "",
webSocketDebuggerUrl: cdpWsUrl,
devtoolsFrontendUrl: `/devtools/inspector.html?ws=${cdpWsUrl.replace("ws://", "")}`
}));
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(list));
return;
}
const activateMatch = path.match(/^\/json\/activate\/(.+)$/);
if (activateMatch && (req.method === "GET" || req.method === "PUT")) {
const targetId = decodeURIComponent(activateMatch[1] ?? "").trim();
if (!targetId) {
res.writeHead(400);
res.end("targetId required");
return;
}
(async () => {
try {
await sendToExtension({
id: nextExtensionId++,
method: "forwardCDPCommand",
params: {
method: "Target.activateTarget",
params: { targetId }
}
});
} catch {}
})();
res.writeHead(200);
res.end("OK");
return;
}
const closeMatch = path.match(/^\/json\/close\/(.+)$/);
if (closeMatch && (req.method === "GET" || req.method === "PUT")) {
const targetId = decodeURIComponent(closeMatch[1] ?? "").trim();
if (!targetId) {
res.writeHead(400);
res.end("targetId required");
return;
}
(async () => {
try {
await sendToExtension({
id: nextExtensionId++,
method: "forwardCDPCommand",
params: {
method: "Target.closeTarget",
params: { targetId }
}
});
} catch {}
})();
res.writeHead(200);
res.end("OK");
return;
}
res.writeHead(404);
res.end("not found");
});
const wssExtension = new WebSocketServer({ noServer: true });
const wssCdp = new WebSocketServer({ noServer: true });
server.on("upgrade", (req, socket, head) => {
const url = new URL(req.url ?? "/", info.baseUrl);
const pathname = url.pathname;
const remote = req.socket.remoteAddress;
if (!isLoopbackAddress(remote)) {
rejectUpgrade(socket, 403, "Forbidden");
return;
}
const origin = headerValue(req.headers.origin);
if (origin && !origin.startsWith("chrome-extension://")) {
rejectUpgrade(socket, 403, "Forbidden: invalid origin");
return;
}
if (pathname === "/extension") {
const token = getRelayAuthTokenFromRequest(req, url);
if (!token || token !== relayAuthToken) {
rejectUpgrade(socket, 401, "Unauthorized");
return;
}
if (extensionWs) {
rejectUpgrade(socket, 409, "Extension already connected");
return;
}
wssExtension.handleUpgrade(req, socket, head, (ws) => {
wssExtension.emit("connection", ws, req);
});
return;
}
if (pathname === "/cdp") {
const token = getRelayAuthTokenFromRequest(req, url);
if (!token || token !== relayAuthToken) {
rejectUpgrade(socket, 401, "Unauthorized");
return;
}
if (!extensionWs) {
rejectUpgrade(socket, 503, "Extension not connected");
return;
}
wssCdp.handleUpgrade(req, socket, head, (ws) => {
wssCdp.emit("connection", ws, req);
});
return;
}
rejectUpgrade(socket, 404, "Not Found");
});
wssExtension.on("connection", (ws) => {
extensionWs = ws;
const ping = setInterval(() => {
if (ws.readyState !== WebSocket$1.OPEN) return;
ws.send(JSON.stringify({ method: "ping" }));
}, 5e3);
ws.on("message", (data) => {
let parsed = null;
try {
parsed = JSON.parse(rawDataToString(data));
} catch {
return;
}
if (parsed && typeof parsed === "object" && "id" in parsed && typeof parsed.id === "number") {
const pending = pendingExtension.get(parsed.id);
if (!pending) return;
pendingExtension.delete(parsed.id);
clearTimeout(pending.timer);
if ("error" in parsed && typeof parsed.error === "string" && parsed.error.trim()) pending.reject(new Error(parsed.error));
else pending.resolve(parsed.result);
return;
}
if (parsed && typeof parsed === "object" && "method" in parsed) {
if (parsed.method === "pong") return;
if (parsed.method !== "forwardCDPEvent") return;
const evt = parsed;
const method = evt.params?.method;
const params = evt.params?.params;
const sessionId = evt.params?.sessionId;
if (!method || typeof method !== "string") return;
if (method === "Target.attachedToTarget") {
const attached = params ?? {};
if ((attached?.targetInfo?.type ?? "page") !== "page") return;
if (attached?.sessionId && attached?.targetInfo?.targetId) {
const prev = connectedTargets.get(attached.sessionId);
const nextTargetId = attached.targetInfo.targetId;
const prevTargetId = prev?.targetId;
const changedTarget = Boolean(prev && prevTargetId && prevTargetId !== nextTargetId);
connectedTargets.set(attached.sessionId, {
sessionId: attached.sessionId,
targetId: nextTargetId,
targetInfo: attached.targetInfo
});
if (changedTarget && prevTargetId) broadcastToCdpClients({
method: "Target.detachedFromTarget",
params: {
sessionId: attached.sessionId,
targetId: prevTargetId
},
sessionId: attached.sessionId
});
if (!prev || changedTarget) broadcastToCdpClients({
method,
params,
sessionId
});
return;
}
}
if (method === "Target.detachedFromTarget") {
const detached = params ?? {};
if (detached?.sessionId) connectedTargets.delete(detached.sessionId);
broadcastToCdpClients({
method,
params,
sessionId
});
return;
}
if (method === "Target.targetInfoChanged") {
const targetInfo = (params ?? {})?.targetInfo;
const targetId = targetInfo?.targetId;
if (targetId && (targetInfo?.type ?? "page") === "page") for (const [sid, target] of connectedTargets) {
if (target.targetId !== targetId) continue;
connectedTargets.set(sid, {
...target,
targetInfo: {
...target.targetInfo,
...targetInfo
}
});
}
}
broadcastToCdpClients({
method,
params,
sessionId
});
}
});
ws.on("close", () => {
clearInterval(ping);
extensionWs = null;
for (const [, pending] of pendingExtension) {
clearTimeout(pending.timer);
pending.reject(/* @__PURE__ */ new Error("extension disconnected"));
}
pendingExtension.clear();
connectedTargets.clear();
for (const client of cdpClients) try {
client.close(1011, "extension disconnected");
} catch {}
cdpClients.clear();
});
});
wssCdp.on("connection", (ws) => {
cdpClients.add(ws);
ws.on("message", async (data) => {
let cmd = null;
try {
cmd = JSON.parse(rawDataToString(data));
} catch {
return;
}
if (!cmd || typeof cmd !== "object") return;
if (typeof cmd.id !== "number" || typeof cmd.method !== "string") return;
if (!extensionWs) {
sendResponseToCdp(ws, {
id: cmd.id,
sessionId: cmd.sessionId,
error: { message: "Extension not connected" }
});
return;
}
try {
const result = await routeCdpCommand(cmd);
if (cmd.method === "Target.setAutoAttach" && !cmd.sessionId) ensureTargetEventsForClient(ws, "autoAttach");
if (cmd.method === "Target.setDiscoverTargets") {
if ((cmd.params ?? {}).discover === true) ensureTargetEventsForClient(ws, "discover");
}
if (cmd.method === "Target.attachToTarget") {
const params = cmd.params ?? {};
const targetId = typeof params.targetId === "string" ? params.targetId : void 0;
if (targetId) {
const target = Array.from(connectedTargets.values()).find((t) => t.targetId === targetId);
if (target) ws.send(JSON.stringify({
method: "Target.attachedToTarget",
params: {
sessionId: target.sessionId,
targetInfo: {
...target.targetInfo,
attached: true
},
waitingForDebugger: false
}
}));
}
}
sendResponseToCdp(ws, {
id: cmd.id,
sessionId: cmd.sessionId,
result
});
} catch (err) {
sendResponseToCdp(ws, {
id: cmd.id,
sessionId: cmd.sessionId,
error: { message: err instanceof Error ? err.message : String(err) }
});
}
});
ws.on("close", () => {
cdpClients.delete(ws);
});
});
try {
await new Promise((resolve, reject) => {
server.listen(info.port, info.host, () => resolve());
server.once("error", reject);
});
} catch (err) {
if (isAddrInUseError(err) && await looksLikeOpenClawRelay(info.baseUrl)) {
const existingRelay = {
host: info.host,
port: info.port,
baseUrl: info.baseUrl,
cdpWsUrl: `ws://${info.host}:${info.port}/cdp`,
extensionConnected: () => false,
stop: async () => {
serversByPort.delete(info.port);
}
};
serversByPort.set(info.port, existingRelay);
return existingRelay;
}
throw err;
}
const port = server.address()?.port ?? info.port;
const host = info.host;
const relay = {
host,
port,
baseUrl: `${new URL(info.baseUrl).protocol}//${host}:${port}`,
cdpWsUrl: `ws://${host}:${port}/cdp`,
extensionConnected: () => Boolean(extensionWs),
stop: async () => {
serversByPort.delete(port);
try {
extensionWs?.close(1001, "server stopping");
} catch {}
for (const ws of cdpClients) try {
ws.close(1001, "server stopping");
} catch {}
await new Promise((resolve) => {
server.close(() => resolve());
});
wssExtension.close();
wssCdp.close();
}
};
serversByPort.set(port, relay);
return relay;
}
async function stopChromeExtensionRelayServer(opts) {
const info = parseBaseUrl(opts.cdpUrl);
const existing = serversByPort.get(info.port);
if (!existing) return false;
await existing.stop();
return true;
}
//#endregion
//#region src/browser/cdp.helpers.ts
function getHeadersWithAuth(url, headers = {}) {
const mergedHeaders = {
...getChromeExtensionRelayAuthHeaders(url),
...headers
};
try {
const parsed = new URL(url);
if (Object.keys(mergedHeaders).some((key) => key.toLowerCase() === "authorization")) return mergedHeaders;
if (parsed.username || parsed.password) {
const auth = Buffer.from(`${parsed.username}:${parsed.password}`).toString("base64");
return {
...mergedHeaders,
Authorization: `Basic ${auth}`
};
}
} catch {}
return mergedHeaders;
}
function appendCdpPath(cdpUrl, path) {
const url = new URL(cdpUrl);
url.pathname = `${url.pathname.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
return url.toString();
}
function createCdpSender(ws) {
let nextId = 1;
const pending = /* @__PURE__ */ new Map();
const send = (method, params, sessionId) => {
const id = nextId++;
const msg = {
id,
method,
params,
sessionId
};
ws.send(JSON.stringify(msg));
return new Promise((resolve, reject) => {
pending.set(id, {
resolve,
reject
});
});
};
const closeWithError = (err) => {
for (const [, p] of pending) p.reject(err);
pending.clear();
try {
ws.close();
} catch {}
};
ws.on("error", (err) => {
closeWithError(err instanceof Error ? err : new Error(String(err)));
});
ws.on("message", (data) => {
try {
const parsed = JSON.parse(rawDataToString(data));
if (typeof parsed.id !== "number") return;
const p = pending.get(parsed.id);
if (!p) return;
pending.delete(parsed.id);
if (parsed.error?.message) {
p.reject(new Error(parsed.error.message));
return;
}
p.resolve(parsed.result);
} catch {}
});
ws.on("close", () => {
closeWithError(/* @__PURE__ */ new Error("CDP socket closed"));
});
return {
send,
closeWithError
};
}
async function fetchJson(url, timeoutMs = 1500, init) {
return await (await fetchChecked(url, timeoutMs, init)).json();
}
async function fetchChecked(url, timeoutMs = 1500, init) {
const ctrl = new AbortController();
const t = setTimeout(ctrl.abort.bind(ctrl), timeoutMs);
try {
const headers = getHeadersWithAuth(url, init?.headers || {});
const res = await fetch(url, {
...init,
headers,
signal: ctrl.signal
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res;
} finally {
clearTimeout(t);
}
}
async function fetchOk(url, timeoutMs = 1500, init) {
await fetchChecked(url, timeoutMs, init);
}
async function withCdpSocket(wsUrl, fn, opts) {
const headers = getHeadersWithAuth(wsUrl, opts?.headers ?? {});
const ws = new WebSocket$1(wsUrl, {
handshakeTimeout: typeof opts?.handshakeTimeoutMs === "number" && Number.isFinite(opts.handshakeTimeoutMs) ? Math.max(1, Math.floor(opts.handshakeTimeoutMs)) : 5e3,
...Object.keys(headers).length ? { headers } : {}
});
const { send, closeWithError } = createCdpSender(ws);
const openPromise = new Promise((resolve, reject) => {
ws.once("open", () => resolve());
ws.once("error", (err) => reject(err));
ws.once("close", () => reject(/* @__PURE__ */ new Error("CDP socket closed")));
});
try {
await openPromise;
} catch (err) {
closeWithError(err instanceof Error ? err : new Error(String(err)));
throw err;
}
try {
return await fn(send);
} catch (err) {
closeWithError(err instanceof Error ? err : new Error(String(err)));
throw err;
} finally {
try {
ws.close();
} catch {}
}
}
//#endregion
//#region src/browser/navigation-guard.ts
const NETWORK_NAVIGATION_PROTOCOLS = new Set(["http:", "https:"]);
var InvalidBrowserNavigationUrlError = class extends Error {
constructor(message) {
super(message);
this.name = "InvalidBrowserNavigationUrlError";
}
};
function withBrowserNavigationPolicy(ssrfPolicy) {
return ssrfPolicy ? { ssrfPolicy } : {};
}
async function assertBrowserNavigationAllowed(opts) {
const rawUrl = String(opts.url ?? "").trim();
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)) return;
await resolvePinnedHostnameWithPolicy(parsed.hostname, {
lookupFn: opts.lookupFn,
policy: opts.ssrfPolicy
});
}
//#endregion
//#region src/browser/cdp.ts
function normalizeCdpWsUrl(wsUrl, cdpUrl) {
const ws = new URL(wsUrl);
const cdp = new URL(cdpUrl);
if (isLoopbackHost(ws.hostname) && !isLoopbackHost(cdp.hostname)) {
ws.hostname = cdp.hostname;
const cdpPort = cdp.port || (cdp.protocol === "https:" ? "443" : "80");
if (cdpPort) ws.port = cdpPort;
ws.protocol = cdp.protocol === "https:" ? "wss:" : "ws:";
}
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();
}
async function captureScreenshot(opts) {
return await withCdpSocket(opts.wsUrl, async (send) => {
await send("Page.enable");
let clip;
if (opts.fullPage) {
const metrics = await send("Page.getLayoutMetrics");
const size = metrics?.cssContentSize ?? metrics?.contentSize;
const width = Number(size?.width ?? 0);
const height = Number(size?.height ?? 0);
if (width > 0 && height > 0) clip = {
x: 0,
y: 0,
width,
height,
scale: 1
};
}
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 } : {},
fromSurface: true,
captureBeyondViewport: true,
...clip ? { clip } : {}
}))?.data;
if (!base64) throw new Error("Screenshot failed: missing data");
return Buffer.from(base64, "base64");
});
}
async function createTargetViaCdp(opts) {
if (!opts.navigationChecked) await assertBrowserNavigationAllowed({
url: opts.url,
...withBrowserNavigationPolicy(opts.ssrfPolicy)
});
const version = await fetchJson(appendCdpPath(opts.cdpUrl, "/json/version"), 1500);
const wsUrlRaw = String(version?.webSocketDebuggerUrl ?? "").trim();
const wsUrl = wsUrlRaw ? normalizeCdpWsUrl(wsUrlRaw, opts.cdpUrl) : "";
if (!wsUrl) throw new Error("CDP /json/version missing webSocketDebuggerUrl");
return await withCdpSocket(wsUrl, async (send) => {
const created = await send("Target.createTarget", { url: opts.url });
const targetId = String(created?.targetId ?? "").trim();
if (!targetId) throw new Error("CDP Target.createTarget returned no targetId");
return { targetId };
});
}
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 "";
}
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();
if (!popped) break;
const { id, depth } = popped;
const n = byId.get(id);
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];
if (child) stack.push({
id: child,
depth: depth + 1
});
}
}
return out;
}
async function snapshotAria(opts) {
const limit = Math.max(1, Math.min(2e3, Math.floor(opts.limit ?? 500)));
return await withCdpSocket(opts.wsUrl, async (send) => {
await send("Accessibility.enable").catch(() => {});
const res = await send("Accessibility.getFullAXTree");
return { nodes: formatAriaSnapshot(Array.isArray(res?.nodes) ? res.nodes : [], limit) };
});
}
//#endregion
//#region src/browser/chrome.executables.ts
const CHROMIUM_BUNDLE_IDS = 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",
"org.chromium.Chromium",
"com.vivaldi.Vivaldi",
"com.operasoftware.Opera",
"com.operasoftware.OperaGX",
"com.yandex.desktop.yandex-browser",
"company.thebrowser.Browser"
]);
const CHROMIUM_DESKTOP_IDS = 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 = new Set([
"chrome.exe",
"msedge.exe",
"brave.exe",
"brave-browser.exe",
"chromium.exe",
"vivaldi.exe",
"opera.exe",
"launcher.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 execText(command, args, timeoutMs = 1200, maxBuffer = 1024 * 1024) {
try {
const output = execFileSync(command, args, {
timeout: timeoutMs,
encoding: "utf8",
maxBuffer
});
return String(output ?? "").trim() || null;
} catch {
return null;
}
}
function inferKindFromIdentifier(identifier) {
const id = identifier.toLowerCase();
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 = name.toLowerCase();
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.trim().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.trim());
if (!exists$1(exePath)) 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, 5 * 1024 * 1024);
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) return null;
const exeName = path.posix.basename(resolved).toLowerCase();
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 (!exists$1(exePath)) return null;
const exeName = path.win32.basename(exePath).toLowerCase();
if (!CHROMIUM_EXE_NAMES.has(exeName)) return null;
return {
kind: inferKindFromExecutableName(exeName),
path: exePath
};
}
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 (let i = 0; i < line.length; i += 1) {
const ch = line[i];
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;
return output.match(/REG_\w+\s+(.+)$/im)?.[1]?.trim() || null;
}
function expandWindowsEnvVars(value) {
return value.replace(/%([^%]+)%/g, (_match, name) => {
const key = String(name ?? "").trim();
return key ? process.env[key] ?? `%${key}%` : _match;
});
}
function extractWindowsExecutablePath(command) {
const quoted = command.match(/"([^"]+\\.exe)"/i);
if (quoted?.[1]) return quoted[1];
const unquoted = command.match(/([^\\s]+\\.exe)/i);
if (unquoted?.[1]) return unquoted[1];
return null;
}
function findFirstExecutable(candidates) {
for (const candidate of candidates) if (exists$1(candidate.path)) return candidate;
return null;
}
function findChromeExecutableMac() {
return findFirstExecutable([
{
kind: "chrome",
path: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
},
{
kind: "chrome",
path: path.join(os.homedir(), "Applications/Google Chrome.app/Contents/MacOS/Google Chrome")
},
{
kind: "brave",
path: "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
},
{
kind: "brave",
path: path.join(os.homedir(), "Applications/Brave Browser.app/Contents/MacOS/Brave Browser")
},
{
kind: "edge",
path: "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"
},
{
kind: "edge",
path: path.join(os.homedir(), "Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge")
},
{
kind: "chromium",
path: "/Applications/Chromium.app/Contents/MacOS/Chromium"
},
{
kind: "chromium",
path: path.join(os.homedir(), "Applications/Chromium.app/Contents/MacOS/Chromium")
},
{
kind: "canary",
path: "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary"
},
{
kind: "canary",
path: path.join(os.homedir(), "Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary")
}
]);
}
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: "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: "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: "/snap/bin/chromium"
}
]);
}
function findChromeExecutableWindows() {
const localAppData = process.env.LOCALAPPDATA ?? "";
const programFiles = process.env.ProgramFiles ?? "C:\\Program Files";
const programFilesX86 = process.env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)";
const joinWin = path.win32.join;
const candidates = [];
if (localAppData) {
candidates.push({
kind: "chrome",
path: joinWin(localAppData, "Google", "Chrome", "Application", "chrome.exe")
});
candidates.push({
kind: "brave",
path: joinWin(localAppData, "BraveSoftware", "Brave-Browser", "Application", "brave.exe")
});
candidates.push({
kind: "edge",
path: joinWin(localAppData, "Microsoft", "Edge", "Application", "msedge.exe")
});
candidates.push({
kind: "chromium",
path: joinWin(localAppData, "Chromium", "Application", "chrome.exe")
});
candidates.push({
kind: "canary",
path: joinWin(localAppData, "Google", "Chrome SxS", "Application", "chrome.exe")
});
}
candidates.push({
kind: "chrome",
path: joinWin(programFiles, "Google", "Chrome", "Application", "chrome.exe")
});
candidates.push({
kind: "chrome",
path: joinWin(programFilesX86, "Google", "Chrome", "Application", "chrome.exe")
});
candidates.push({
kind: "brave",
path: joinWin(programFiles, "BraveSoftware", "Brave-Browser", "Application", "brave.exe")
});
candidates.push({
kind: "brave",
path: joinWin(programFilesX86, "BraveSoftware", "Brave-Browser", "Application", "brave.exe")
});
candidates.push({
kind: "edge",
path: joinWin(programFiles, "Microsoft", "Edge", "Application", "msedge.exe")
});
candidates.push({
kind: "edge",
path: joinWin(programFilesX86, "Microsoft", "Edge", "Application", "msedge.exe")
});
return findFirstExecutable(candidates);
}
function resolveBrowserExecutableForPlatform(resolved, platform) {
if (resolved.executablePath) {
if (!exists$1(resolved.executablePath)) throw new Error(`browser.executablePath not found: ${resolved.executablePath}`);
return {
kind: "custom",
path: resolved.executablePath
};
}
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 src/infra/ports-lsof.ts
const LSOF_CANDIDATES = process.platform === "darwin" ? ["/usr/sbin/lsof", "/usr/bin/lsof"] : ["/usr/bin/lsof", "/usr/sbin/lsof"];
//#endregion
//#region src/infra/ports.ts
var PortInUseError = class extends Error {
constructor(port, details) {
super(`Port ${port} is already in use.`);
this.name = "PortInUseError";
this.port = port;
this.details = details;
}
};
async function ensurePortAvailable(port) {
try {
await new Promise((resolve, reject) => {
const tester = net.createServer().once("error", (err) => reject(err)).once("listening", () => {
tester.close(() => resolve());
}).listen(port);
});
} catch (err) {
if (isErrno(err) && err.code === "EADDRINUSE") throw new PortInUseError(port);
throw err;
}
}
//#endregion
//#region src/browser/chrome.profile-decoration.ts
function decoratedMarkerPath(userDataDir) {
return path.join(userDataDir, ".openclaw-profile-decorated");
}
function safeReadJson(filePath) {
try {
if (!fs.existsSync(filePath)) return null;
const raw = fs.readFileSync(filePath, "utf-8");
const parsed = JSON.parse(raw);
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
return parsed;
} catch {
return null;
}
}
function safeWriteJson(filePath, data) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
}
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;
}
function isProfileDecorated(userDataDir, desiredName, desiredColorHex) {
const desiredColorInt = parseHexRgbToSignedArgbInt(desiredColorHex);
const localStatePath = path.join(userDataDir, "Local State");
const preferencesPath = path.join(userDataDir, "Default", "Preferences");
const profile = safeReadJson(localStatePath)?.profile;
const infoCache = typeof profile === "object" && profile !== null && !Array.isArray(profile) ? profile.info_cache : null;
const info = typeof infoCache === "object" && infoCache !== null && !Array.isArray(infoCache) && typeof infoCache.Default === "object" && infoCache.Default !== null && !Array.isArray(infoCache.Default) ? infoCache.Default : null;
const prefs = safeReadJson(preferencesPath);
const browserTheme = (() => {
const browser = prefs?.browser;
const theme = typeof browser === "object" && browser !== null && !Array.isArray(browser) ? browser.theme : null;
return typeof theme === "object" && theme !== null && !Array.isArray(theme) ? theme : null;
})();
const autogeneratedTheme = (() => {
const autogenerated = prefs?.autogenerated;
const theme = typeof autogenerated === "object" && autogenerated !== null && !Array.isArray(autogenerated) ? autogenerated.theme : null;
return typeof theme === "object" && theme !== null && !Array.isArray(theme) ? theme : null;
})();
const nameOk = typeof info?.name === "string" ? info.name === desiredName : true;
if (desiredColorInt == null) return nameOk;
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;
}
/**
* 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 ?? DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME;
const desiredColor = (opts?.color ?? DEFAULT_OPENCLAW_BROWSER_COLOR).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);
}
safeWriteJson(preferencesPath, prefs);
try {
fs.writeFileSync(decoratedMarkerPath(userDataDir), `${Date.now()}\n`, "utf-8");
} catch {}
}
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 src/browser/chrome.ts
const log = createSubsystemLogger("browser").child("chrome");
function exists(filePath) {
try {
return fs.existsSync(filePath);
} catch {
return false;
}
}
function resolveBrowserExecutable(resolved) {
return resolveBrowserExecutableForPlatform(resolved, process