openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
585 lines (584 loc) • 24.9 kB
JavaScript
import { c as normalizeOptionalString, s as normalizeOptionalLowercaseString } from "./string-coerce-mnp54Vah.js";
import { n as MAX_TIMER_TIMEOUT_MS } from "./number-coercion-CJQ8TR--.js";
import { d as resolveGatewayPort } from "./paths-mvMm5bYV.js";
import { a as normalizeOptionalTrimmedStringList } from "./string-normalization-WNUDCpXX.js";
import { d as resolveExistingPathsWithinRoot, m as resolveStrictExistingPathsWithinRoot } from "./fs-safe-aqmM_n6V.js";
import { p as resolveUserPath, t as CONFIG_DIR } from "./utils-CCC-BEJH.js";
import { n as resolvePreferredOpenClawTmpDir } from "./tmp-openclaw-dir-DOKojISm.js";
import { i as isLoopbackHost } from "./net-DTe7AQiu.js";
import "./number-runtime-DBLVDypr.js";
import "./runtime-config-snapshot-CDoFLjqb.js";
import "./string-coerce-runtime-CEGJWkQ_.js";
import "./text-utility-runtime-BZ3jGr-e.js";
import "./core-DSxVv-v1.js";
import "./plugin-config-runtime-CpE6DYgJ.js";
import "./config-mutation-B38RpU_9.js";
import { t as parseBrowserHttpUrl } from "./browser-config-CB1dJM4Y.js";
import "./sdk-security-runtime-D1sDHmUD.js";
import { G as DEFAULT_OPENCLAW_BROWSER_COLOR, H as DEFAULT_BROWSER_LOCAL_LAUNCH_TIMEOUT_MS, R as DEFAULT_BROWSER_ACTION_TIMEOUT_MS, V as DEFAULT_BROWSER_LOCAL_CDP_READY_TIMEOUT_MS, q as DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME } from "./cdp.helpers-CTQ-1blZ.js";
import "./tmp-openclaw-dir-BvAQDjrf.js";
import path from "node:path";
import fs from "node:fs/promises";
import os from "node:os";
//#region extensions/browser/src/sdk-config.ts
/**
* Browser-local SDK config bridge plus Browser-specific default port helpers.
*/
const DEFAULT_BROWSER_CDP_PORT_RANGE_START$1 = 18800;
const DEFAULT_BROWSER_CDP_PORT_RANGE_END = 18899;
const DEFAULT_BROWSER_CDP_PORT_RANGE_SPAN = DEFAULT_BROWSER_CDP_PORT_RANGE_END - DEFAULT_BROWSER_CDP_PORT_RANGE_START$1;
/** Default loopback port for the Browser control server. */
const DEFAULT_BROWSER_CONTROL_PORT = 18791;
function isValidPort(port) {
return Number.isFinite(port) && port > 0 && port <= 65535;
}
function clampPort(port, fallback) {
return isValidPort(port) ? port : fallback;
}
function derivePort(base, offset, fallback) {
return clampPort(base + offset, fallback);
}
/** Derives the Browser control port from the gateway port. */
function deriveDefaultBrowserControlPort(gatewayPort) {
return derivePort(gatewayPort, 2, DEFAULT_BROWSER_CONTROL_PORT);
}
/** Derives the managed Chrome CDP port range from the Browser control port. */
function deriveDefaultBrowserCdpPortRange(browserControlPort) {
const start = derivePort(browserControlPort, 9, DEFAULT_BROWSER_CDP_PORT_RANGE_START$1);
const end = start + DEFAULT_BROWSER_CDP_PORT_RANGE_SPAN;
if (end <= 65535) return {
start,
end
};
return {
start: DEFAULT_BROWSER_CDP_PORT_RANGE_START$1,
end: DEFAULT_BROWSER_CDP_PORT_RANGE_END
};
}
const DEFAULT_TRUTHY = [
"true",
"1",
"yes",
"on"
];
const DEFAULT_FALSY = [
"false",
"0",
"no",
"off"
];
function matchesBooleanToken(value, tokens) {
return tokens.includes(value);
}
/** Parses common string booleans with optional custom truthy/falsy tokens. */
function parseBooleanValue(value, options = {}) {
if (typeof value === "boolean") return value;
if (typeof value !== "string") return;
const normalized = normalizeOptionalLowercaseString(value);
if (!normalized) return;
const candidates = [[true, options.truthy ?? DEFAULT_TRUTHY], [false, options.falsy ?? DEFAULT_FALSY]];
for (const [parsed, tokens] of candidates) if (matchesBooleanToken(normalized, tokens)) return parsed;
}
//#endregion
//#region extensions/browser/src/browser/paths.ts
/**
* Browser filesystem path helpers.
*
* Defines browser output roots and resolves upload/media references while
* enforcing root-scoped path access for Browser tool file inputs.
*/
const DEFAULT_FALLBACK_BROWSER_TMP_DIR = "/tmp/openclaw";
function canUseNodeFs() {
const getBuiltinModule = process.getBuiltinModule;
if (typeof getBuiltinModule !== "function") return false;
try {
return getBuiltinModule("fs") !== void 0;
} catch {
return false;
}
}
const DEFAULT_BROWSER_TMP_DIR = canUseNodeFs() ? resolvePreferredOpenClawTmpDir() : DEFAULT_FALLBACK_BROWSER_TMP_DIR;
/** Default root directory for browser trace files. */
const DEFAULT_TRACE_DIR = DEFAULT_BROWSER_TMP_DIR;
/** Default root directory for browser downloads. */
const DEFAULT_DOWNLOAD_DIR = path.join(DEFAULT_BROWSER_TMP_DIR, "downloads");
/** Default root directory for browser upload inputs. */
const DEFAULT_UPLOAD_DIR = path.join(DEFAULT_BROWSER_TMP_DIR, "uploads");
/** Default root directory for managed inbound media references. */
const DEFAULT_INBOUND_MEDIA_DIR = path.join(CONFIG_DIR, "media", "inbound");
function normalizeUploadPathSource(source) {
const trimmed = source.trim();
if (/^media:\/\//i.test(trimmed)) return trimmed;
return trimmed.replace(/^\s*MEDIA\s*:\s*/i, "").trim();
}
function decodeInboundMediaId(value, source) {
let id;
try {
id = decodeURIComponent(value);
} catch {
return {
ok: false,
error: `Invalid media reference: ${source}`
};
}
if (!id || id === "." || id === ".." || id.includes("/") || id.includes("\\") || id.includes("\0")) return {
ok: false,
error: `Invalid media reference: ${source}`
};
return {
ok: true,
path: id
};
}
function resolveManagedInboundMediaRef(source, inboundMediaDir) {
const normalizedSource = normalizeUploadPathSource(source);
if (!normalizedSource) return null;
if (/^media:\/\//i.test(normalizedSource)) {
const rawPath = /^media:\/\/[^/?#]*([^?#]*)/iu.exec(normalizedSource)?.[1] ?? "";
let parsed;
try {
parsed = new URL(normalizedSource);
} catch {
return {
ok: false,
error: `Invalid media reference: ${normalizedSource}`
};
}
if (parsed.hostname !== "inbound") return {
ok: false,
error: `Unsupported media reference location: ${parsed.hostname || "(missing)"}`
};
if (!rawPath.startsWith("/") || rawPath.slice(1).includes("/") || rawPath.includes("\\")) return {
ok: false,
error: `Invalid media reference: ${normalizedSource}`
};
const decoded = decodeInboundMediaId(rawPath.slice(1), normalizedSource);
return decoded?.ok ? {
ok: true,
path: path.join(inboundMediaDir, decoded.path),
uploadRootPrecedence: false
} : decoded;
}
const relativeMatch = /^(?:\.\/)?media\/inbound\/([^/\\]+)$/u.exec(normalizedSource);
if (!relativeMatch?.[1]) return null;
const decoded = decodeInboundMediaId(relativeMatch[1], normalizedSource);
return decoded?.ok ? {
ok: true,
path: path.join(inboundMediaDir, decoded.path),
uploadRootPrecedence: true
} : decoded;
}
async function isDirectInboundMediaFile(params) {
let inboundRoot;
try {
inboundRoot = await fs.realpath(params.inboundMediaDir);
} catch {
inboundRoot = path.resolve(params.inboundMediaDir);
}
const relativePath = path.relative(inboundRoot, params.resolvedPath);
return Boolean(relativePath) && relativePath !== ".." && !relativePath.startsWith(`..${path.sep}`) && !path.isAbsolute(relativePath) && !relativePath.includes("/") && !relativePath.includes("\\");
}
async function resolveDirectInboundMediaPath(params) {
const inboundPathsResult = params.strict ? await resolveStrictExistingPathsWithinRoot({
rootDir: params.inboundMediaDir,
requestedPaths: [params.requestedPath],
scopeLabel: `inbound media directory (${params.inboundMediaDir})`
}) : await resolveExistingPathsWithinRoot({
rootDir: params.inboundMediaDir,
requestedPaths: [params.requestedPath],
scopeLabel: `inbound media directory (${params.inboundMediaDir})`
});
if (!inboundPathsResult.ok) return inboundPathsResult;
const resolvedPath = inboundPathsResult.paths[0] ?? params.requestedPath;
if (!await isDirectInboundMediaFile({
inboundMediaDir: params.inboundMediaDir,
resolvedPath
})) return {
ok: false,
error: `Invalid media reference: must be a direct child of inbound media directory (${params.inboundMediaDir})`
};
return inboundPathsResult;
}
/** Resolve upload paths and managed media references into existing file paths. */
async function resolveExistingUploadPaths({ requestedPaths, uploadDir = DEFAULT_UPLOAD_DIR, inboundMediaDir = DEFAULT_INBOUND_MEDIA_DIR }) {
const paths = [];
for (const requestedPath of requestedPaths) {
const managedMediaPathResult = resolveManagedInboundMediaRef(requestedPath, inboundMediaDir);
if (managedMediaPathResult?.ok === false) return managedMediaPathResult;
if (managedMediaPathResult?.uploadRootPrecedence !== false) {
const uploadPathsResult = managedMediaPathResult?.uploadRootPrecedence === true ? await resolveStrictExistingPathsWithinRoot({
rootDir: uploadDir,
requestedPaths: [requestedPath],
scopeLabel: `uploads directory (${uploadDir})`
}) : await resolveExistingPathsWithinRoot({
rootDir: uploadDir,
requestedPaths: [requestedPath],
scopeLabel: `uploads directory (${uploadDir})`
});
if (uploadPathsResult.ok) {
paths.push(uploadPathsResult.paths[0] ?? requestedPath);
continue;
}
}
const inboundPathsResult = await resolveDirectInboundMediaPath({
inboundMediaDir,
requestedPath: managedMediaPathResult?.path ?? requestedPath,
strict: false
});
if (!inboundPathsResult.ok) return inboundPathsResult;
paths.push(inboundPathsResult.paths[0] ?? requestedPath);
}
return {
ok: true,
paths
};
}
/** Strictly resolve upload paths under the upload root only. */
async function resolveStrictExistingUploadPaths({ requestedPaths, uploadDir = DEFAULT_UPLOAD_DIR, inboundMediaDir = DEFAULT_INBOUND_MEDIA_DIR }) {
const paths = [];
for (const requestedPath of requestedPaths) {
const managedMediaPathResult = resolveManagedInboundMediaRef(requestedPath, inboundMediaDir);
if (managedMediaPathResult?.ok === false) return managedMediaPathResult;
if (managedMediaPathResult?.uploadRootPrecedence !== false) {
const uploadPathsResult = await resolveStrictExistingPathsWithinRoot({
rootDir: uploadDir,
requestedPaths: [requestedPath],
scopeLabel: `uploads directory (${uploadDir})`
});
if (uploadPathsResult.ok) {
paths.push(uploadPathsResult.paths[0] ?? requestedPath);
continue;
}
}
const inboundPathsResult = await resolveDirectInboundMediaPath({
inboundMediaDir,
requestedPath: managedMediaPathResult?.path ?? requestedPath,
strict: true
});
if (!inboundPathsResult.ok) return inboundPathsResult;
paths.push(inboundPathsResult.paths[0] ?? requestedPath);
}
return {
ok: true,
paths
};
}
//#endregion
//#region extensions/browser/src/browser/config.ts
/**
* Browser config resolution.
*
* Normalizes raw browser config into resolved runtime defaults, profile
* records, SSRF policy, timeouts, headless mode, and managed Chrome settings.
*/
const DEFAULT_BROWSER_CDP_PORT_RANGE_START = 18800;
const MAX_BROWSER_STARTUP_TIMEOUT_MS = 12e4;
/** Environment variable that overrides managed Chrome headless mode. */
const OPENCLAW_BROWSER_HEADLESS_ENV = "OPENCLAW_BROWSER_HEADLESS";
function normalizeHexColor(raw) {
const value = (raw ?? "").trim();
if (!value) return DEFAULT_OPENCLAW_BROWSER_COLOR;
const normalized = value.startsWith("#") ? value : `#${value}`;
if (!/^#[0-9a-fA-F]{6}$/.test(normalized)) return DEFAULT_OPENCLAW_BROWSER_COLOR;
return normalized.toUpperCase();
}
function normalizeTimeoutMs(raw, fallback) {
const value = typeof raw === "number" && Number.isFinite(raw) ? Math.floor(raw) : fallback;
return value < 0 ? fallback : value;
}
function normalizeStartupTimeoutMs(raw, fallback) {
const value = typeof raw === "number" && Number.isFinite(raw) ? Math.floor(raw) : fallback;
if (value <= 0) return fallback;
return Math.min(value, MAX_BROWSER_STARTUP_TIMEOUT_MS);
}
function normalizeNonNegativeInteger(raw, fallback) {
const value = typeof raw === "number" && Number.isFinite(raw) ? Math.floor(raw) : fallback;
return value < 0 ? fallback : value;
}
function normalizePositiveInteger(raw, fallback) {
const value = typeof raw === "number" && Number.isFinite(raw) ? Math.floor(raw) : fallback;
return value <= 0 ? fallback : value;
}
const MAX_BROWSER_TIMER_MINUTES = Math.floor(MAX_TIMER_TIMEOUT_MS / 6e4);
function normalizeNonNegativeTimerMinutes(raw, fallback) {
return Math.min(normalizeNonNegativeInteger(raw, fallback), MAX_BROWSER_TIMER_MINUTES);
}
function normalizePositiveTimerMinutes(raw, fallback) {
return Math.min(normalizePositiveInteger(raw, fallback), MAX_BROWSER_TIMER_MINUTES);
}
function normalizeExecutablePath(raw) {
const value = normalizeOptionalString(raw);
if (!value) return;
if (!/^~(?=$|[\\/])/.test(value)) return value;
return path.resolve(value.replace(/^~(?=$|[\\/])/, os.homedir()));
}
function normalizeExistingSessionCdpUrl(raw, profileName) {
const value = normalizeOptionalString(raw);
if (!value) return;
let parsed;
try {
parsed = new URL(value);
} catch {
throw new Error(`browser.profiles.${profileName}.cdpUrl must be a valid URL.`);
}
if (![
"http:",
"https:",
"ws:",
"wss:"
].includes(parsed.protocol)) throw new Error(`browser.profiles.${profileName}.cdpUrl must use http, https, ws, or wss.`);
return {
cdpUrl: parsed.protocol === "http:" || parsed.protocol === "https:" ? parsed.toString().replace(/\/$/, "") : parsed.toString(),
cdpHost: parsed.hostname,
cdpIsLoopback: isLoopbackHost(parsed.hostname)
};
}
function hasLinuxDisplay(env) {
return Boolean(env.DISPLAY?.trim() || env.WAYLAND_DISPLAY?.trim());
}
function isLocalManagedProfile(profile) {
return profile.driver === "openclaw" && profile.cdpIsLoopback && !profile.attachOnly;
}
function resolveBrowserTabCleanupConfig(cfg) {
const raw = cfg?.tabCleanup;
return {
enabled: raw?.enabled ?? true,
idleMinutes: normalizeNonNegativeTimerMinutes(raw?.idleMinutes, 120),
maxTabsPerSession: normalizeNonNegativeInteger(raw?.maxTabsPerSession, 8),
sweepMinutes: normalizePositiveTimerMinutes(raw?.sweepMinutes, 5)
};
}
function resolveCdpPortRangeStart(rawStart, fallbackStart, rangeSpan) {
const start = typeof rawStart === "number" && Number.isFinite(rawStart) ? Math.floor(rawStart) : fallbackStart;
if (start < 1 || start > 65535) throw new Error(`browser.cdpPortRangeStart must be between 1 and 65535, got: ${start}`);
const maxStart = 65535 - rangeSpan;
if (start > maxStart) throw new Error(`browser.cdpPortRangeStart (${start}) is too high for a ${rangeSpan + 1}-port range; max is ${maxStart}.`);
return start;
}
const normalizeStringList = normalizeOptionalTrimmedStringList;
function resolveBrowserSsrFPolicy(cfg) {
const rawPolicy = cfg?.ssrfPolicy;
const allowPrivateNetwork = rawPolicy?.allowPrivateNetwork;
const dangerouslyAllowPrivateNetwork = rawPolicy?.dangerouslyAllowPrivateNetwork;
const allowedHostnames = normalizeStringList(rawPolicy?.allowedHostnames);
const hostnameAllowlist = normalizeStringList(rawPolicy?.hostnameAllowlist);
const hasExplicitPrivateSetting = allowPrivateNetwork !== void 0 || dangerouslyAllowPrivateNetwork !== void 0;
const resolvedAllowPrivateNetwork = dangerouslyAllowPrivateNetwork === true || allowPrivateNetwork === true;
if (!resolvedAllowPrivateNetwork && !hasExplicitPrivateSetting && !allowedHostnames && !hostnameAllowlist) return {};
return {
...resolvedAllowPrivateNetwork || dangerouslyAllowPrivateNetwork === false || allowPrivateNetwork === false ? { dangerouslyAllowPrivateNetwork: resolvedAllowPrivateNetwork } : {},
...allowedHostnames ? { allowedHostnames } : {},
...hostnameAllowlist ? { hostnameAllowlist } : {}
};
}
function ensureDefaultProfile(profiles, defaultColor, legacyCdpPort, derivedDefaultCdpPort, legacyCdpUrl) {
const result = { ...profiles };
if (!result["openclaw"]) result[DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME] = {
cdpPort: legacyCdpPort ?? derivedDefaultCdpPort ?? DEFAULT_BROWSER_CDP_PORT_RANGE_START,
color: defaultColor,
...legacyCdpUrl ? { cdpUrl: legacyCdpUrl } : {}
};
return result;
}
function ensureDefaultUserBrowserProfile(profiles) {
const result = { ...profiles };
if (result.user) return result;
result.user = {
driver: "existing-session",
attachOnly: true,
color: "#00AA00"
};
return result;
}
function applyLegacyCdpUrlToExistingSessionDefaultProfile(profiles, defaultProfile, legacyCdpUrl) {
if (!legacyCdpUrl) return profiles;
const profile = profiles[defaultProfile];
if (!profile || profile.driver !== "existing-session" || normalizeOptionalString(profile.cdpUrl)) return profiles;
return {
...profiles,
[defaultProfile]: {
...profile,
cdpUrl: legacyCdpUrl
}
};
}
/** Resolve raw browser config into runtime browser defaults. */
function resolveBrowserConfig(cfg, rootConfig) {
const enabled = cfg?.enabled ?? true;
const evaluateEnabled = cfg?.evaluateEnabled ?? true;
const controlPort = deriveDefaultBrowserControlPort(resolveGatewayPort(rootConfig) ?? 18791);
const defaultColor = normalizeHexColor(cfg?.color);
const remoteCdpTimeoutMs = normalizeTimeoutMs(cfg?.remoteCdpTimeoutMs, 1500);
const remoteCdpHandshakeTimeoutMs = normalizeTimeoutMs(cfg?.remoteCdpHandshakeTimeoutMs, Math.max(2e3, remoteCdpTimeoutMs * 2));
const localLaunchTimeoutMs = normalizeStartupTimeoutMs(cfg?.localLaunchTimeoutMs, DEFAULT_BROWSER_LOCAL_LAUNCH_TIMEOUT_MS);
const localCdpReadyTimeoutMs = normalizeStartupTimeoutMs(cfg?.localCdpReadyTimeoutMs, DEFAULT_BROWSER_LOCAL_CDP_READY_TIMEOUT_MS);
const actionTimeoutMs = normalizeTimeoutMs(cfg?.actionTimeoutMs, DEFAULT_BROWSER_ACTION_TIMEOUT_MS);
const derivedCdpRange = deriveDefaultBrowserCdpPortRange(controlPort);
const cdpRangeSpan = derivedCdpRange.end - derivedCdpRange.start;
const cdpPortRangeStart = resolveCdpPortRangeStart(cfg?.cdpPortRangeStart, derivedCdpRange.start, cdpRangeSpan);
const cdpPortRangeEnd = cdpPortRangeStart + cdpRangeSpan;
const rawCdpUrl = (cfg?.cdpUrl ?? "").trim();
let cdpInfo;
if (rawCdpUrl) cdpInfo = parseBrowserHttpUrl(rawCdpUrl, "browser.cdpUrl");
else {
const derivedPort = controlPort + 1;
if (derivedPort > 65535) throw new Error(`Derived CDP port (${derivedPort}) is too high; check gateway port configuration.`);
const derived = new URL(`http://127.0.0.1:${derivedPort}`);
cdpInfo = {
parsed: derived,
port: derivedPort,
normalized: derived.toString().replace(/\/$/, "")
};
}
const headless = cfg?.headless === true;
const headlessSource = typeof cfg?.headless === "boolean" ? "config" : "default";
const noSandbox = cfg?.noSandbox === true;
const attachOnly = cfg?.attachOnly === true;
const executablePath = normalizeExecutablePath(cfg?.executablePath);
const defaultProfileFromConfig = normalizeOptionalString(cfg?.defaultProfile);
const legacyCdpPort = rawCdpUrl ? cdpInfo.port : void 0;
const isWsUrl = cdpInfo.parsed.protocol === "ws:" || cdpInfo.parsed.protocol === "wss:";
const legacyCdpUrl = rawCdpUrl && isWsUrl ? cdpInfo.normalized : void 0;
let profiles = ensureDefaultUserBrowserProfile(ensureDefaultProfile(cfg?.profiles, defaultColor, legacyCdpPort, cdpPortRangeStart, legacyCdpUrl));
const cdpProtocol = cdpInfo.parsed.protocol === "https:" ? "https" : "http";
const defaultProfile = defaultProfileFromConfig ?? (profiles["openclaw"] ? "openclaw" : profiles["openclaw"] ? "openclaw" : "user");
profiles = applyLegacyCdpUrlToExistingSessionDefaultProfile(profiles, defaultProfile, rawCdpUrl ? cdpInfo.normalized : void 0);
const extraArgs = Array.isArray(cfg?.extraArgs) ? cfg.extraArgs.filter((value) => typeof value === "string" && value.trim().length > 0) : [];
return {
enabled,
evaluateEnabled,
controlPort,
cdpPortRangeStart,
cdpPortRangeEnd,
cdpProtocol,
cdpHost: cdpInfo.parsed.hostname,
cdpIsLoopback: isLoopbackHost(cdpInfo.parsed.hostname),
remoteCdpTimeoutMs,
remoteCdpHandshakeTimeoutMs,
localLaunchTimeoutMs,
localCdpReadyTimeoutMs,
actionTimeoutMs,
color: defaultColor,
executablePath,
headless,
headlessSource,
noSandbox,
attachOnly,
defaultProfile,
profiles,
tabCleanup: resolveBrowserTabCleanupConfig(cfg),
ssrfPolicy: resolveBrowserSsrFPolicy(cfg),
extraArgs
};
}
/** Resolve one configured browser profile by name. */
function resolveProfile(resolved, profileName) {
const profile = resolved.profiles[profileName];
if (!profile) return null;
const rawProfileUrl = profile.cdpUrl?.trim() ?? "";
let cdpHost = resolved.cdpHost;
let cdpPort = profile.cdpPort ?? 0;
let cdpUrl;
const driver = profile.driver === "existing-session" ? "existing-session" : "openclaw";
const headless = profile.headless ?? resolved.headless;
const headlessSource = typeof profile.headless === "boolean" ? "profile" : resolved.headlessSource;
const executablePath = normalizeExecutablePath(profile.executablePath) ?? resolved.executablePath;
if (driver === "existing-session") {
const existingSessionCdp = normalizeExistingSessionCdpUrl(rawProfileUrl, profileName);
return {
name: profileName,
cdpPort: 0,
cdpUrl: existingSessionCdp?.cdpUrl ?? "",
cdpHost: existingSessionCdp?.cdpHost ?? "",
cdpIsLoopback: existingSessionCdp?.cdpIsLoopback ?? true,
userDataDir: resolveUserPath(profile.userDataDir?.trim() || "") || void 0,
mcpCommand: normalizeOptionalString(profile.mcpCommand),
mcpArgs: normalizeStringList(profile.mcpArgs) ?? void 0,
color: profile.color,
driver,
executablePath,
headless,
headlessSource,
attachOnly: true
};
}
if (rawProfileUrl !== "" && cdpPort > 0 && /^wss?:\/\//i.test(rawProfileUrl) && /\/devtools\/browser\//i.test(rawProfileUrl)) {
cdpHost = new URL(rawProfileUrl).hostname;
cdpUrl = `${resolved.cdpProtocol}://${cdpHost}:${cdpPort}`;
} else if (rawProfileUrl) {
const parsed = parseBrowserHttpUrl(rawProfileUrl, `browser.profiles.${profileName}.cdpUrl`);
cdpHost = parsed.parsed.hostname;
if (parsed.hasExplicitPort) {
cdpPort = parsed.port;
cdpUrl = parsed.normalizedWithPort;
} else if (cdpPort) {
const rebuilt = new URL(rawProfileUrl);
rebuilt.port = String(cdpPort);
cdpUrl = rebuilt.toString().replace(/\/$/, "");
} else {
cdpPort = parsed.port;
cdpUrl = parsed.normalized;
}
} else if (cdpPort) cdpUrl = `${resolved.cdpProtocol}://${resolved.cdpHost}:${cdpPort}`;
else throw new Error(`Profile "${profileName}" must define cdpPort or cdpUrl.`);
return {
name: profileName,
cdpPort,
cdpUrl,
cdpHost,
cdpIsLoopback: isLoopbackHost(cdpHost),
color: profile.color,
driver,
executablePath,
headless,
headlessSource,
attachOnly: profile.attachOnly ?? resolved.attachOnly
};
}
/** Resolve effective headless mode for a managed browser profile. */
function resolveManagedBrowserHeadlessMode(resolved, profile, params = {}) {
if (!isLocalManagedProfile(profile)) return {
headless: profile.headless,
source: profile.headlessSource ?? "default"
};
if (typeof params.headlessOverride === "boolean") return {
headless: params.headlessOverride,
source: "request"
};
const env = params.env ?? process.env;
const platform = params.platform ?? process.platform;
const envHeadless = parseBooleanValue(env[OPENCLAW_BROWSER_HEADLESS_ENV]);
if (envHeadless !== void 0) return {
headless: envHeadless,
source: "env"
};
const profileHeadlessSource = profile.headlessSource ?? "default";
if (profileHeadlessSource !== "default") return {
headless: profile.headless,
source: profileHeadlessSource
};
if (platform === "linux" && !hasLinuxDisplay(env)) return {
headless: true,
source: "linux-display-fallback"
};
return {
headless: resolved.headless,
source: "default"
};
}
/** Return a Linux display error for headed managed Chrome when no display exists. */
function getManagedBrowserMissingDisplayError(resolved, profile, params = {}) {
if (!isLocalManagedProfile(profile)) return null;
const env = params.env ?? process.env;
const platform = params.platform ?? process.platform;
if (platform !== "linux" || hasLinuxDisplay(env)) return null;
const mode = resolveManagedBrowserHeadlessMode(resolved, profile, {
env,
platform
});
if (mode.headless) return null;
const sourceHint = mode.source === "request" ? "request override" : mode.source === "env" ? `${OPENCLAW_BROWSER_HEADLESS_ENV}=0` : mode.source === "profile" ? `browser.profiles.${profile.name}.headless=false` : "browser.headless=false";
return `Headed browser start requested for profile "${profile.name}" via ${sourceHint}, but no Linux display server was detected (\$DISPLAY/\$WAYLAND_DISPLAY unset). Set ${OPENCLAW_BROWSER_HEADLESS_ENV}=1, remove the headed override, or launch under Xvfb.`;
}
//#endregion
export { DEFAULT_DOWNLOAD_DIR as a, resolveExistingUploadPaths as c, parseBooleanValue as d, resolveProfile as i, resolveStrictExistingUploadPaths as l, resolveBrowserConfig as n, DEFAULT_TRACE_DIR as o, resolveManagedBrowserHeadlessMode as r, DEFAULT_UPLOAD_DIR as s, getManagedBrowserMissingDisplayError as t, deriveDefaultBrowserCdpPortRange as u };