UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

1,428 lines 62.5 kB
import { l as normalizeOptionalString, o as normalizeLowercaseStringOrEmpty } from "./string-coerce-CIXf7egm.js";
import { n as getRuntimeConfig } from "./io.runtime-B9iJRs3w.js";
import { s as sleepWithAbort } from "./src-BQ327IOM.js";
import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js";
import { l as getRuntimeConfigSourceSnapshot } from "./runtime-snapshot-BaQikjTR.js";
import "./runtime-env-0Q-gCyUb.js";
import "./string-coerce-runtime-GQa0ehRA.js";
import { n as redactCdpUrl } from "./browser-cdp-DfRHhv02.js";
import { c as DEFAULT_BROWSER_LOCAL_CDP_READY_TIMEOUT_MS } from "./constants-DKjTvIBP.js";
import { a as resolveProfile, n as getOwnBrowserProfile, r as resolveBrowserConfig } from "./config-BosO4Zbt.js";
import { c as BrowserResetUnsupportedError, d as BrowserTargetAmbiguousError, m as toBrowserErrorResponse, n as BROWSER_ERROR_REASONS, o as BrowserProfileNotFoundError, s as BrowserProfileUnavailableError, u as BrowserTabNotFoundError } from "./errors-6hPg4OWh.js";
import { t as getBrowserProfileCapabilities } from "./profile-capabilities-B30ig3n7.js";
import { t as getExtensionRelayModule } from "./extension-relay.runtime.js";
import { _ as getPwAiModule, a as getOrCreateProfileRuntime, c as isProfileGenerationCurrent, f as registerProfileHandle, h as withProfileOperationLease, i as enqueueProfileStart, l as isProfileRestartRequiredError, m as waitForProfileOperation, n as assertProfileLifecycleContext, o as getProfileLifecycle, p as releaseProfileHandle, r as beginProfileTransition, s as isBrowserRuntimeRunning, t as ProfileRestartRequiredError, u as isWithinProfileOperationLease, v as getChromeMcpModule } from "./server-context.lifecycle-Tmt_A8dN.js";
import { B as requiresInspectableBrowserNavigationRedirectsForUrl, F as InvalidBrowserNavigationUrlError, I as assertBrowserNavigationAllowed, R as assertBrowserNavigationResultAllowed, S as normalizeCdpWsUrl, U as waitForCdpCommittedNavigationUrl, V as withBrowserNavigationPolicy, W as resolveBrowserNavigationProxyMode, a as isChromeReachable, c as stopOpenClawChrome, g as formatChromeCdpDiagnostic, h as diagnoseChromeCdp, i as isChromeCdpReady, l as stopOwnedOpenClawChrome, o as launchOpenClawChrome, r as isChromeCdpOwnedByPid, s as resolveOpenClawUserDataDir, t as ManagedChromeCleanupError, y as createTargetViaCdp } from "./chrome-CtUCaDWQ.js";
import { C as CHROME_MCP_ATTACH_READY_WINDOW_MS, D as PROFILE_ATTACH_RETRY_TIMEOUT_MS, O as resolveCdpReachabilityTimeouts, a as fetchJson, d as resolveCdpTabOwnership, k as usesFastLoopbackCdpProbeClass, l as normalizeCdpHttpBaseForJsonEndpoints, n as assertCdpEndpointAllowed, o as fetchOk, t as appendCdpPath } from "./cdp.helpers-CLv_y5y4.js";
import "./errors-CesLEJB8.js";
import "./config-C0cjs7WS.js";
import { n as resolveCdpControlPolicy, r as resolveCdpReachabilityPolicy, t as assertChromeMcpCdpTransportAllowed } from "./cdp-reachability-policy-2SFcQJH2.js";
import { t as movePathToTrash } from "./trash-BWuUMhs8.js";
import fs from "node:fs";
import { isDeepStrictEqual } from "node:util";
//#region extensions/browser/src/browser/config-refresh-source.ts
/**
* Browser runtime config refresh source.
*
* Loads the source-backed runtime config snapshot when available so long-lived
* browser routes can refresh from disk without changing config ownership.
*/
/** Load the best available config object for browser route runtime refresh. */
function loadBrowserConfigForRuntimeRefresh() {
	return getRuntimeConfigSourceSnapshot() ?? getRuntimeConfig();
}
//#endregion
//#region extensions/browser/src/browser/resolved-config-refresh.ts
/**
* Runtime config refresh helpers for Browser profiles that can be hot-reloaded
* without restarting the whole Browser plugin server.
*/
function changedProfileInvariants(current, next, previousConfig, nextConfig) {
	const changed = [];
	const currentUsesLocalManagedLaunch = current.driver === "openclaw" && !current.attachOnly && current.cdpIsLoopback;
	const nextUsesLocalManagedLaunch = next.driver === "openclaw" && !next.attachOnly && next.cdpIsLoopback;
	if (current.cdpUrl !== next.cdpUrl) changed.push("cdpUrl");
	if (current.cdpPort !== next.cdpPort) changed.push("cdpPort");
	if (current.driver !== next.driver) changed.push("driver");
	if (currentUsesLocalManagedLaunch && nextUsesLocalManagedLaunch) {
		if (current.headless !== next.headless) changed.push("headless");
		if (current.executablePath !== next.executablePath) changed.push("executablePath");
		if (previousConfig.noSandbox !== nextConfig.noSandbox) changed.push("noSandbox");
		if (!isDeepStrictEqual(previousConfig.extraArgs, nextConfig.extraArgs)) changed.push("extraArgs");
	}
	if (current.attachOnly !== next.attachOnly) changed.push("attachOnly");
	if (current.cdpIsLoopback !== next.cdpIsLoopback) changed.push("cdpIsLoopback");
	if ((current.userDataDir ?? "") !== (next.userDataDir ?? "")) changed.push("userDataDir");
	if ((current.mcpCommand ?? "") !== (next.mcpCommand ?? "")) changed.push("mcpCommand");
	if (!isDeepStrictEqual(current.mcpArgs, next.mcpArgs)) changed.push("mcpArgs");
	return changed;
}
function queueRemovedProfileCleanup(params) {
	const actor = getProfileLifecycle(params.runtime);
	if (!params.initial && (!actor.blockedReason || actor.transitionReason)) return;
	params.runtime.lastTargetId = null;
	beginProfileTransition({
		state: params.current,
		runtime: params.runtime,
		reason: params.initial ? "profile removed from config" : "profile removal cleanup retry",
		terminal: "config-removed",
		advanceConfigRevision: params.initial,
		closeRelay: params.runtime.profile.driver === "extension",
		exposeReason: true
	}).then(() => {
		if (params.current.profiles.get(params.name) === params.runtime) params.current.profiles.delete(params.name);
	}).catch(() => {});
}
function applyResolvedConfig(current, freshResolved) {
	const previousResolved = current.resolved;
	const extensionRelayInternalTokens = {};
	for (const [name, relay] of current.extensionRelays ?? []) {
		const runtime = current.profiles.get(name);
		const lifecycle = runtime && getProfileLifecycle(runtime);
		const profile = resolveProfile(freshResolved, name);
		if (runtime?.profile.driver === "extension" && !lifecycle?.terminal && !lifecycle?.transitionReason && !lifecycle?.cleanupRelays.has(relay) && profile?.driver === "extension" && profile.cdpPort === relay.port && relay.ownership !== "borrowed") extensionRelayInternalTokens[name] = relay.internalToken;
	}
	current.resolved = {
		...freshResolved,
		enabled: previousResolved.enabled,
		evaluateEnabled: previousResolved.evaluateEnabled,
		ssrfPolicy: previousResolved.ssrfPolicy,
		extensionRelay: previousResolved.extensionRelay,
		extensionRelayInternalTokens,
		extensionRelayToken: previousResolved.extensionRelayToken
	};
	for (const [name, runtime] of current.profiles) {
		const actor = getProfileLifecycle(runtime);
		if (actor.terminal === "config-removed") {
			queueRemovedProfileCleanup({
				current,
				name,
				runtime,
				initial: false
			});
			continue;
		}
		if (actor.terminal) continue;
		const nextProfile = resolveProfile(current.resolved, name);
		if (nextProfile) {
			if (actor.blockedReason && !actor.transitionReason) {
				beginProfileTransition({
					state: current,
					runtime,
					reason: "profile invariant cleanup retry",
					captureProfileResources: false,
					exposeReason: true
				}).catch(() => {});
				continue;
			}
			const changed = changedProfileInvariants(runtime.profile, nextProfile, previousResolved, freshResolved);
			if (changed.length > 0) {
				const previousProfile = runtime.profile;
				const reason = `profile invariants changed: ${changed.join(", ")}`;
				beginProfileTransition({
					state: current,
					runtime,
					reason,
					advanceConfigRevision: true,
					closeRelay: previousProfile.driver === "extension",
					exposeReason: true
				}).catch(() => {});
				runtime.lastTargetId = null;
			}
			runtime.profile = nextProfile;
			continue;
		}
		queueRemovedProfileCleanup({
			current,
			name,
			runtime,
			initial: true
		});
	}
}
/** Refreshes the Browser runtime's resolved config from disk when hot reload is enabled. */
function refreshResolvedBrowserConfigFromDisk(params) {
	if (!params.refreshConfigFromDisk) return;
	const cfg = loadBrowserConfigForRuntimeRefresh();
	const freshResolved = resolveBrowserConfig(cfg.browser, cfg);
	applyResolvedConfig(params.current, freshResolved);
}
//#endregion
//#region extensions/browser/src/browser/server-context.constants.ts
/**
* Timing and size constants for Browser profile/tab runtime operations.
*/
const OPEN_TAB_DISCOVERY_WINDOW_MS = 2e3;
const CDP_READY_AFTER_LAUNCH_WINDOW_MS = DEFAULT_BROWSER_LOCAL_CDP_READY_TIMEOUT_MS;
//#endregion
//#region extensions/browser/src/browser/server-context.availability.ts
/**
* Browser profile availability operations: reachability probes, managed Chrome
* launch/restart, Chrome MCP attach, and profile stop handling.
*/
const MANAGED_LAUNCH_FAILURE_THRESHOLD = 3;
const MANAGED_LAUNCH_COOLDOWN_BASE_MS = 3e4;
const MANAGED_LAUNCH_COOLDOWN_MAX_MS = 3e5;
function launchOptionsForEnsure(options) {
	return typeof options?.headless === "boolean" ? { headlessOverride: options.headless } : void 0;
}
function ensureOptionsKey(options) {
	return typeof options?.headless === "boolean" ? `headless:${options.headless}` : "default";
}
function formatLocalPortOwnershipHint(profile) {
	const resetHint = `If OpenClaw should own this local profile, run action=reset-profile profile=${profile.name} to stop the conflicting process.`;
	if (!profile.cdpIsLoopback) return resetHint;
	return `${resetHint} If this port is an externally managed CDP service such as Browserless, set browser.profiles.${profile.name}.attachOnly=true so OpenClaw attaches without trying to manage the local process. For Browserless Docker, set EXTERNAL to the same WebSocket endpoint OpenClaw can reach via browser.profiles.<name>.cdpUrl.`;
}
function normalizeFailureMessage(err) {
	return (err instanceof Error ? err.message : String(err)).trim() || "unknown browser launch failure";
}
function resetManagedLaunchFailure(profileState) {
	profileState.managedLaunchFailure = void 0;
}
function recordManagedLaunchFailure(profileState, err) {
	const consecutiveFailures = (profileState.managedLaunchFailure?.consecutiveFailures ?? 0) + 1;
	const exponent = Math.max(0, consecutiveFailures - MANAGED_LAUNCH_FAILURE_THRESHOLD);
	const cooldownMs = consecutiveFailures >= MANAGED_LAUNCH_FAILURE_THRESHOLD ? Math.min(MANAGED_LAUNCH_COOLDOWN_MAX_MS, MANAGED_LAUNCH_COOLDOWN_BASE_MS * 2 ** exponent) : 0;
	const now = Date.now();
	profileState.managedLaunchFailure = {
		consecutiveFailures,
		lastFailureAt: now,
		...cooldownMs > 0 ? { cooldownUntil: now + cooldownMs } : {},
		lastError: normalizeFailureMessage(err)
	};
}
function assertManagedLaunchNotCoolingDown(profileName, profileState) {
	const failure = profileState.managedLaunchFailure;
	if (!failure || failure.consecutiveFailures < MANAGED_LAUNCH_FAILURE_THRESHOLD) return;
	const remainingMs = (failure.cooldownUntil ?? 0) - Date.now();
	if (remainingMs <= 0) return;
	const retrySeconds = Math.max(1, Math.ceil(remainingMs / 1e3));
	throw new BrowserProfileUnavailableError(`Browser launch for profile "${profileName}" is cooling down after ${failure.consecutiveFailures} consecutive managed Chrome launch failures. Retry in ${retrySeconds}s after fixing Chrome startup, or set browser.enabled=false if the browser tool is not needed. Last error: ${failure.lastError}`);
}
/** Builds reachability, ensure, and stop operations for one resolved browser profile. */
function createProfileAvailability({ opts, profile, state, runtime, configRevision }) {
	const redactedProfileCdpUrl = redactCdpUrl(profile.cdpUrl) ?? profile.cdpUrl;
	const capabilities = getBrowserProfileCapabilities(profile);
	const resolveTimeouts = (timeoutMs) => resolveCdpReachabilityTimeouts({
		profileIsLoopback: profile.cdpIsLoopback,
		attachOnly: profile.attachOnly,
		timeoutMs,
		remoteHttpTimeoutMs: state().resolved.remoteCdpTimeoutMs,
		remoteHandshakeTimeoutMs: state().resolved.remoteCdpHandshakeTimeoutMs
	});
	const getCdpReachabilityPolicy = () => resolveCdpReachabilityPolicy(profile, state().resolved.ssrfPolicy);
	const ensureExtensionRelay = async (signal) => {
		signal?.throwIfAborted();
		if (capabilities.mode !== "local-extension") return;
		const { ensureExtensionRelayForProfile } = await getExtensionRelayModule();
		const relay = await ensureExtensionRelayForProfile(state(), profile, signal);
		signal?.throwIfAborted();
		return relay;
	};
	const isReachable = async (timeoutMs, options) => {
		const relay = await ensureExtensionRelay(options?.signal);
		if (relay?.ownership === "borrowed") {
			const ready = await relay.client.status();
			options?.signal?.throwIfAborted();
			return ready.ready && state().extensionRelays?.get(profile.name) === relay;
		}
		if (capabilities.usesChromeMcp) {
			assertChromeMcpCdpTransportAllowed(profile, getCdpReachabilityPolicy());
			const { countChromeMcpTabs } = await getChromeMcpModule();
			await countChromeMcpTabs(profile.name, profile, {
				...timeoutMs != null ? { timeoutMs } : {},
				...options?.ephemeral ? { ephemeral: true } : {},
				...options?.signal ? { signal: options.signal } : {}
			});
			return true;
		}
		const { httpTimeoutMs, wsTimeoutMs } = resolveTimeouts(timeoutMs);
		return await isChromeCdpReady(profile.cdpUrl, httpTimeoutMs, wsTimeoutMs, getCdpReachabilityPolicy());
	};
	const isTransportAvailable = async (timeoutMs, signal, pageProbe) => {
		if (capabilities.usesChromeMcp) {
			assertChromeMcpCdpTransportAllowed(profile, getCdpReachabilityPolicy());
			const { ensureChromeMcpAvailable } = await getChromeMcpModule();
			await ensureChromeMcpAvailable(profile.name, profile, {
				ephemeral: true,
				timeoutMs,
				signal,
				...pageProbe ? { pageProbe } : {}
			});
			return true;
		}
		return await isReachable(timeoutMs, { signal });
	};
	const isHttpReachable = async (timeoutMs, signal) => {
		if (capabilities.usesChromeMcp) return await isTransportAvailable(timeoutMs, signal);
		const relay = await ensureExtensionRelay(signal);
		if (relay?.ownership === "borrowed") {
			const ready = await relay.client.status();
			signal?.throwIfAborted();
			return ready.ready && state().extensionRelays?.get(profile.name) === relay;
		}
		const { httpTimeoutMs } = resolveTimeouts(timeoutMs);
		return await isChromeReachable(profile.cdpUrl, httpTimeoutMs, getCdpReachabilityPolicy());
	};
	const describeCdpFailure = async (timeoutMs) => {
		const { httpTimeoutMs, wsTimeoutMs } = resolveTimeouts(timeoutMs);
		const diagnostic = await diagnoseChromeCdp(profile.cdpUrl, httpTimeoutMs, wsTimeoutMs, getCdpReachabilityPolicy());
		return formatChromeCdpDiagnostic(diagnostic);
	};
	const stopExactRunning = async (profileState, running) => {
		try {
			await stopOpenClawChrome(running);
			releaseProfileHandle(profileState, running);
		} catch (err) {
			getProfileLifecycle(profileState).blockedReason = "managed Chrome cleanup failed";
			throw err;
		}
	};
	const adoptRunning = (params) => {
		const actor = getProfileLifecycle(params.profileState);
		if (!isProfileGenerationCurrent({
			state: state(),
			runtime: params.profileState,
			configRevision,
			generation: params.generation
		})) {
			params.signal.throwIfAborted();
			throw new BrowserProfileUnavailableError(`Browser start for profile "${profile.name}" was superseded.`);
		}
		if (!actor.handles.has(params.running) || params.running.proc.exitCode != null || params.running.proc.signalCode != null) throw new BrowserProfileUnavailableError(`Managed Chrome for profile "${profile.name}" exited before adoption.`);
		params.profileState.running = params.running;
	};
	const formatChromeMcpAttachFailure = (lastError) => {
		const detail = lastError instanceof Error ? ` Last error: ${lastError.message}` : "";
		const message = lastError instanceof Error ? lastError.message : "";
		if (message.includes("DevToolsActivePort") || message.includes("Could not connect to Chrome")) return `Chrome MCP existing-session attach for profile "${profile.name}" could not connect to Chrome. Enable remote debugging in the browser inspect page, keep the browser open, approve the attach prompt, and retry. If you do not need your signed-in browser session, use the managed "openclaw" profile instead.` + detail;
		return `Chrome MCP existing-session attach for profile "${profile.name}" timed out waiting for tabs to become available. Approve the browser attach prompt, keep the browser open, and retry.${detail}`;
	};
	const waitForPoll = async (delayMs, signal) => {
		signal.throwIfAborted();
		await new Promise((resolve, reject) => {
			const finish = () => {
				signal.removeEventListener("abort", onAbort);
				resolve();
			};
			const timer = setTimeout(finish, delayMs);
			const onAbort = () => {
				clearTimeout(timer);
				signal.removeEventListener("abort", onAbort);
				reject(signal.reason instanceof Error ? signal.reason : new Error("Browser availability wait aborted.", { cause: signal.reason }));
			};
			signal.addEventListener("abort", onAbort, { once: true });
			if (signal.aborted) onAbort();
		});
	};
	const waitForCdpReadyAfterLaunch = async (signal, running) => {
		const deadlineMs = Date.now() + (state().resolved.localCdpReadyTimeoutMs ?? CDP_READY_AFTER_LAUNCH_WINDOW_MS);
		while (Date.now() < deadlineMs) {
			const remainingMs = Math.max(0, deadlineMs - Date.now());
			const attemptTimeoutMs = Math.max(75, Math.min(250, remainingMs));
			signal.throwIfAborted();
			if (await isReachable(attemptTimeoutMs, { signal })) {
				const ownsEndpoint = await isChromeCdpOwnedByPid(profile.cdpUrl, running.pid, attemptTimeoutMs, getCdpReachabilityPolicy());
				signal.throwIfAborted();
				if (!ownsEndpoint) throw new BrowserProfileUnavailableError(`Managed Chrome for profile "${profile.name}" did not own its CDP endpoint.`);
				return;
			}
			await waitForPoll(100, signal);
		}
		throw new Error(`Chrome CDP websocket for profile "${profile.name}" is not reachable after start. ${await describeCdpFailure(250)}`);
	};
	const waitForChromeMcpReadyAfterAttach = async (signal) => {
		const deadlineMs = Date.now() + CHROME_MCP_ATTACH_READY_WINDOW_MS;
		let lastError;
		while (Date.now() < deadlineMs) {
			try {
				const { listChromeMcpTabs } = await getChromeMcpModule();
				await listChromeMcpTabs(profile.name, profile, { signal });
				return;
			} catch (err) {
				lastError = err;
			}
			signal.throwIfAborted();
			await waitForPoll(200, signal);
		}
		throw new BrowserProfileUnavailableError(formatChromeMcpAttachFailure(lastError));
	};
	const launchManagedChrome = async (profileState, current, launchOptions, signal) => {
		assertManagedLaunchNotCoolingDown(profile.name, profileState);
		try {
			return await launchOpenClawChrome(current.resolved, profile, {
				...launchOptions,
				signal
			});
		} catch (err) {
			if (err instanceof ManagedChromeCleanupError) {
				if (registerProfileHandle(profileState, err.running)) getProfileLifecycle(profileState).blockedReason = "managed Chrome cleanup failed";
				throw err;
			}
			if (signal.aborted) throw err;
			if (!(err instanceof BrowserProfileUnavailableError && err.metadata?.reason === BROWSER_ERROR_REASONS.noDisplayForHeadedProfile)) recordManagedLaunchFailure(profileState, err);
			throw err;
		}
	};
	const ensureBrowserAvailableOnce = async (signal, generation, options) => {
		signal.throwIfAborted();
		if (capabilities.usesChromeMcp) {
			if (profile.userDataDir && !fs.existsSync(profile.userDataDir)) throw new BrowserProfileUnavailableError(`Browser user data directory not found for profile "${profile.name}": ${profile.userDataDir}`);
			assertChromeMcpCdpTransportAllowed(profile, getCdpReachabilityPolicy());
			const { ensureChromeMcpAvailable } = await getChromeMcpModule();
			await ensureChromeMcpAvailable(profile.name, profile, { signal });
			await waitForChromeMcpReadyAfterAttach(signal);
			return;
		}
		const current = state();
		const remoteCdp = capabilities.isRemote;
		const attachOnly = profile.attachOnly;
		let httpReachable;
		if (capabilities.mode === "local-extension") {
			const { ensureExtensionRelayForProfile } = await getExtensionRelayModule();
			const relay = await ensureExtensionRelayForProfile(current, profile, signal);
			const connected = relay.ownership === "borrowed" ? (await waitForProfileOperation(relay.client.status(CHROME_MCP_ATTACH_READY_WINDOW_MS), signal)).ready : await relay.bridge.waitForExtensionConnection(signal, CHROME_MCP_ATTACH_READY_WINDOW_MS);
			signal.throwIfAborted();
			httpReachable = connected && current.extensionRelays?.get(profile.name) === relay && await isHttpReachable(void 0, signal);
		} else httpReachable = await isHttpReachable(void 0, signal);
		const launchOptions = launchOptionsForEnsure(options);
		if (!httpReachable) {
			if ((attachOnly || remoteCdp) && opts.onEnsureAttachTarget) {
				await opts.onEnsureAttachTarget(profile);
				signal.throwIfAborted();
				if (await isHttpReachable(1200, signal)) return;
			}
			if (!attachOnly && !remoteCdp && profile.cdpIsLoopback && !runtime.running) {
				if (await isHttpReachable(1200, signal) && await isReachable(1200, { signal })) {
					resetManagedLaunchFailure(runtime);
					return;
				}
			}
			if (attachOnly || remoteCdp) {
				if (capabilities.mode === "local-extension") {
					const { EXTENSION_PAIRING_HINT } = await getExtensionRelayModule();
					throw new BrowserProfileUnavailableError(`The OpenClaw Chrome extension is not connected for profile "${profile.name}". Open Chrome on this machine and check the extension popup shows "Connected". ${EXTENSION_PAIRING_HINT}`);
				}
				throw new BrowserProfileUnavailableError(remoteCdp ? `Remote CDP for profile "${profile.name}" is not reachable at ${redactedProfileCdpUrl}.` : `Browser attachOnly is enabled and profile "${profile.name}" is not running.`);
			}
			if (runtime.running) throw new ProfileRestartRequiredError();
			const launched = await launchManagedChrome(runtime, current, launchOptions, signal);
			if (!registerProfileHandle(runtime, launched)) throw new BrowserProfileUnavailableError(`Managed Chrome for profile "${profile.name}" exited before adoption.`);
			try {
				await waitForCdpReadyAfterLaunch(signal, launched);
				adoptRunning({
					profileState: runtime,
					running: launched,
					generation,
					signal
				});
				resetManagedLaunchFailure(runtime);
			} catch (err) {
				await stopExactRunning(runtime, launched);
				if (!signal.aborted) recordManagedLaunchFailure(runtime, err);
				throw err;
			}
			return;
		}
		if (await isReachable(void 0, { signal })) {
			resetManagedLaunchFailure(runtime);
			return;
		}
		if (attachOnly || remoteCdp) {
			if (opts.onEnsureAttachTarget) {
				await opts.onEnsureAttachTarget(profile);
				signal.throwIfAborted();
				if (await isReachable(1200, { signal })) return;
			}
			if (remoteCdp && await isReachable(1200, { signal })) return;
			if (capabilities.mode === "local-extension") {
				const { EXTENSION_PAIRING_HINT } = await getExtensionRelayModule();
				throw new BrowserProfileUnavailableError(`The extension relay for profile "${profile.name}" is running but the OpenClaw Chrome extension is not connected. ${EXTENSION_PAIRING_HINT}`);
			}
			const detail = await describeCdpFailure(PROFILE_ATTACH_RETRY_TIMEOUT_MS);
			throw new BrowserProfileUnavailableError(remoteCdp ? `Remote CDP websocket for profile "${profile.name}" is not reachable. ${detail}` : `Browser attachOnly is enabled and CDP websocket for profile "${profile.name}" is not reachable. ${detail}`);
		}
		if (!runtime.running) {
			const detail = await describeCdpFailure(PROFILE_ATTACH_RETRY_TIMEOUT_MS);
			throw new BrowserProfileUnavailableError(`Port ${profile.cdpPort} is in use for profile "${profile.name}" but not by openclaw. ${formatLocalPortOwnershipHint(profile)} ${detail}`);
		}
		throw new ProfileRestartRequiredError();
	};
	const ensureBrowserAvailable = async (options) => {
		if (capabilities.mode === "local-extension") {
			await withProfileOperationLease({
				state: state(),
				runtime,
				configRevision,
				signal: options?.signal,
				run: async (signal) => await ensureBrowserAvailableOnce(signal, getProfileLifecycle(runtime).generation, options)
			});
			return;
		}
		const key = ensureOptionsKey(options);
		for (;;) try {
			await enqueueProfileStart({
				state: state(),
				runtime,
				configRevision,
				key,
				signal: options?.signal,
				run: async (signal, generation) => {
					await ensureBrowserAvailableOnce(signal, generation, options);
				}
			});
			return;
		} catch (err) {
			if (!isProfileRestartRequiredError(err)) throw err;
			if (isWithinProfileOperationLease(runtime)) throw err;
			await beginProfileTransition({
				state: state(),
				runtime,
				reason: "managed Chrome restart required"
			});
		}
	};
	const stopRunningBrowser = async () => {
		const current = state();
		assertProfileLifecycleContext({
			state: current,
			runtime,
			configRevision
		});
		resetManagedLaunchFailure(runtime);
		let stoppedOwnedProcess = false;
		return { stopped: (await beginProfileTransition({
			state: current,
			runtime,
			reason: "stop requested",
			afterCleanup: async () => {
				if (profile.attachOnly || capabilities.mode !== "local-managed") return;
				stoppedOwnedProcess = await stopOwnedOpenClawChrome(current.resolved, profile);
			}
		})).stopped || stoppedOwnedProcess || profile.attachOnly || capabilities.isRemote };
	};
	return {
		isHttpReachable,
		isTransportAvailable,
		isReachable,
		ensureBrowserAvailable,
		stopRunningBrowser
	};
}
//#endregion
//#region extensions/browser/src/browser/server-context.reset.ts
/**
* Browser profile reset operations for local managed profiles.
*/
/** Builds the reset-profile operation for one resolved browser profile. */
function createProfileResetOps({ profile, state, runtime, configRevision, resolveOpenClawUserDataDir }) {
	const capabilities = getBrowserProfileCapabilities(profile);
	const resetProfile = async () => {
		if (!capabilities.supportsReset) throw new BrowserResetUnsupportedError(`reset-profile is only supported for local profiles (profile "${profile.name}" is remote).`);
		const userDataDir = resolveOpenClawUserDataDir(profile.name);
		assertProfileLifecycleContext({
			state: state(),
			runtime,
			configRevision
		});
		runtime.managedLaunchFailure = void 0;
		let result = {
			moved: false,
			from: userDataDir
		};
		await beginProfileTransition({
			state: state(),
			runtime,
			reason: "profile reset requested",
			afterCleanup: async () => {
				if (!fs.existsSync(userDataDir)) return;
				const moved = await movePathToTrash(userDataDir);
				result = {
					moved: true,
					from: userDataDir,
					to: moved
				};
			}
		});
		return result;
	};
	return { resetProfile };
}
//#endregion
//#region extensions/browser/src/browser/target-id.ts
/**
* Target id resolution helpers for Browser tab aliases and user-facing ids.
*/
const TAB_LABEL_PATTERN = /^[A-Za-z0-9_.:-]{1,64}$/;
/** Validate and normalize a user-facing tab label before browser mutation. */
function normalizeTabLabel(label) {
	const trimmed = label.trim();
	if (!TAB_LABEL_PATTERN.test(trimmed)) throw new Error("tab label must be 1-64 chars and use only letters, numbers, _, ., :, or -");
	return trimmed;
}
function getTabAliasState(profileState) {
	profileState.tabAliases ??= {
		nextTabNumber: 1,
		byTargetId: {}
	};
	return profileState.tabAliases;
}
/** Assign a stable friendly id and optional validated label to one tab. */
function assignTabAlias(params) {
	const label = params.label === void 0 ? void 0 : normalizeTabLabel(params.label);
	const aliases = getTabAliasState(params.profileState);
	let entry = aliases.byTargetId[params.tab.targetId];
	if (!entry) {
		entry = { tabId: `t${aliases.nextTabNumber}` };
		aliases.nextTabNumber += 1;
		aliases.byTargetId[params.tab.targetId] = entry;
	}
	if (label) {
		for (const [targetId, current] of Object.entries(aliases.byTargetId)) if (targetId !== params.tab.targetId && current.label === label) delete current.label;
		entry.label = label;
	}
	entry.url = params.tab.url;
	const labelFields = entry.label ? { label: entry.label } : {};
	return {
		...params.tab,
		suggestedTargetId: entry.label ?? entry.tabId,
		tabId: entry.tabId,
		...labelFields
	};
}
function normalizeReplacementUrl(url) {
	return url?.trim() || void 0;
}
function findConfidentReplacement(params) {
	const { staleEntry, staleEntries, newCandidates } = params;
	if (staleEntries.length === 1 && newCandidates.length === 1) return newCandidates[0];
	const url = normalizeReplacementUrl(staleEntry.url);
	if (!url) return;
	const staleMatches = staleEntries.filter(([, entry]) => normalizeReplacementUrl(entry.url) === url);
	const candidates = newCandidates.filter((tab) => normalizeReplacementUrl(tab.url) === url);
	return staleMatches.length === 1 && candidates.length === 1 ? candidates[0] : void 0;
}
/** Reconcile stable aliases with the latest authoritative browser tab list. */
function assignTabAliases(profileState, tabs, migrateReplacements) {
	const aliases = getTabAliasState(profileState);
	const liveTargetIds = new Set(tabs.map((tab) => tab.targetId));
	const staleEntries = Object.entries(aliases.byTargetId).filter(([targetId]) => !liveTargetIds.has(targetId));
	const newCandidates = tabs.filter((tab) => !aliases.byTargetId[tab.targetId]);
	if (migrateReplacements) for (const [oldTargetId, staleEntry] of staleEntries) {
		const candidate = findConfidentReplacement({
			staleEntry,
			staleEntries,
			newCandidates
		});
		if (!candidate) continue;
		aliases.byTargetId[candidate.targetId] = staleEntry;
		delete aliases.byTargetId[oldTargetId];
		if (profileState.lastTargetId === oldTargetId) profileState.lastTargetId = candidate.targetId;
	}
	for (const targetId of Object.keys(aliases.byTargetId)) if (!liveTargetIds.has(targetId)) delete aliases.byTargetId[targetId];
	return tabs.map((tab) => assignTabAlias({
		profileState,
		tab
	}));
}
/** Resolves exact tab references first, then unique raw target-id prefixes. */
function resolveTargetIdFromTabs(input, tabs) {
	const needle = input.trim();
	if (!needle) return {
		ok: false,
		reason: "not_found"
	};
	const exactMatches = [...new Set(tabs.filter((tab) => tab.targetId === needle || tab.suggestedTargetId === needle || tab.tabId === needle || tab.label === needle).map((tab) => tab.targetId))];
	const onlyExact = exactMatches[0];
	if (exactMatches.length === 1 && onlyExact !== void 0) return {
		ok: true,
		targetId: onlyExact
	};
	if (exactMatches.length > 1) return {
		ok: false,
		reason: "ambiguous",
		matches: exactMatches
	};
	const lower = normalizeLowercaseStringOrEmpty(needle);
	const matches = tabs.map((t) => t.targetId).filter((id) => normalizeLowercaseStringOrEmpty(id).startsWith(lower));
	const only = matches.length === 1 ? matches[0] : void 0;
	if (only) return {
		ok: true,
		targetId: only
	};
	if (matches.length === 0) return {
		ok: false,
		reason: "not_found"
	};
	return {
		ok: false,
		reason: "ambiguous",
		matches
	};
}
//#endregion
//#region extensions/browser/src/browser/server-context.selection.ts
/**
* Browser tab selection operations for default tab choice, focus, and close.
*/
function mergeOpenedTabSnapshot(tabs, openedTab) {
	if (!openedTab) return tabs;
	const index = tabs.findIndex((tab) => tab.targetId === openedTab.targetId);
	if (index < 0) return [...tabs, openedTab];
	const listedTab = tabs[index];
	if (!listedTab || listedTab.wsUrl || !openedTab.wsUrl) return tabs;
	const merged = tabs.slice();
	merged[index] = {
		...listedTab,
		wsUrl: openedTab.wsUrl,
		...openedTab.wsLookup ? { wsLookup: openedTab.wsLookup } : {}
	};
	return merged;
}
/** Builds tab selection/focus/close operations for one resolved browser profile. */
function createProfileSelectionOps({ profile, runtime, getCdpControlPolicy, ensureBrowserAvailable, listTabs, openTab }) {
	const cdpHttpBase = normalizeCdpHttpBaseForJsonEndpoints(profile.cdpUrl);
	const capabilities = getBrowserProfileCapabilities(profile);
	const ensureTabAvailable = async (targetId, options, browserAlreadyEnsured = false) => {
		options?.signal?.throwIfAborted();
		if (!browserAlreadyEnsured) await ensureBrowserAvailable({ signal: options?.signal });
		options?.signal?.throwIfAborted();
		let lastNonEmptyTabs = [];
		let lastListError;
		let sawSuccessfulList = false;
		let openedTab;
		const readTabs = async () => {
			try {
				const tabs = await listTabs(options);
				options?.signal?.throwIfAborted();
				sawSuccessfulList = true;
				if (tabs.length > 0) lastNonEmptyTabs = tabs;
				return tabs;
			} catch (err) {
				options?.signal?.throwIfAborted();
				lastListError = err;
				return [];
			}
		};
		const openWhenConfirmedEmpty = async (tabs) => {
			if (!openedTab && sawSuccessfulList && lastNonEmptyTabs.length === 0 && tabs.length === 0) openedTab = await openTab("about:blank", options);
		};
		const candidateTabs = (tabs) => capabilities.supportsPerTabWs ? tabs.filter((tab) => Boolean(tab.wsUrl)) : tabs;
		const canResolveSelection = (tabs) => {
			const desiredTargetId = targetId ?? openedTab?.targetId ?? normalizeOptionalString(runtime.lastTargetId) ?? void 0;
			if (!desiredTargetId) return tabs.length > 0;
			if (targetId === void 0) return tabs.some((tab) => tab.targetId === desiredTargetId);
			const resolved = resolveTargetIdFromTabs(desiredTargetId, tabs);
			return resolved.ok || resolved.reason === "ambiguous";
		};
		await openWhenConfirmedEmpty(await readTabs());
		let listedTabs = await readTabs();
		await openWhenConfirmedEmpty(listedTabs);
		let unfilteredTabs = mergeOpenedTabSnapshot(listedTabs, openedTab);
		let candidates = candidateTabs(unfilteredTabs);
		const preservedCanResolveSelection = () => canResolveSelection(mergeOpenedTabSnapshot(lastNonEmptyTabs, openedTab));
		if (capabilities.supportsPerTabWs && !canResolveSelection(candidates) && (candidates.length === 0 || canResolveSelection(unfilteredTabs) || preservedCanResolveSelection())) {
			const deadline = Date.now() + OPEN_TAB_DISCOVERY_WINDOW_MS;
			while (Date.now() < deadline) {
				await sleepWithAbort(100, options?.signal);
				listedTabs = await readTabs();
				await openWhenConfirmedEmpty(listedTabs);
				unfilteredTabs = mergeOpenedTabSnapshot(listedTabs, openedTab);
				candidates = candidateTabs(unfilteredTabs);
				if (canResolveSelection(candidates)) break;
			}
		}
		if (!canResolveSelection(candidates)) {
			const preservedTabs = mergeOpenedTabSnapshot(lastNonEmptyTabs, openedTab);
			const preservedCandidates = candidateTabs(preservedTabs);
			if (canResolveSelection(preservedCandidates)) candidates = preservedCandidates;
			else if (options?.allowPlaywrightFallback && canResolveSelection(preservedTabs)) candidates = preservedTabs;
		}
		if (candidates.length === 0 && !sawSuccessfulList && lastListError) throw lastListError instanceof Error ? lastListError : new Error(formatErrorMessage(lastListError));
		const resolveById = (raw, targetOptions) => {
			if (targetOptions?.exactTargetId) return candidates.find((tab) => tab.targetId === raw) ?? null;
			const resolved = resolveTargetIdFromTabs(raw, candidates);
			if (!resolved.ok) {
				if (resolved.reason === "ambiguous") return "AMBIGUOUS";
				return null;
			}
			return candidates.find((t) => t.targetId === resolved.targetId) ?? null;
		};
		const stickyTargetId = normalizeOptionalString(runtime.lastTargetId);
		const pickDefault = () => {
			const last = stickyTargetId ?? "";
			const lastResolved = last ? resolveById(last, { exactTargetId: true }) : null;
			if (lastResolved && lastResolved !== "AMBIGUOUS") return lastResolved;
			if (last) return null;
			return candidates.find((t) => (t.type ?? "page") === "page") ?? candidates.at(0) ?? null;
		};
		const chosen = targetId ? resolveById(targetId) : pickDefault();
		if (chosen === "AMBIGUOUS") throw new BrowserTargetAmbiguousError();
		if (!chosen) throw new BrowserTabNotFoundError({ input: targetId ?? stickyTargetId });
		runtime.lastTargetId = chosen.targetId;
		return chosen;
	};
	const resolveTargetIdOrThrow = async (targetId, options) => {
		const tabs = await listTabs(options);
		if (options?.exactTargetId) {
			const exactTarget = tabs.find((tab) => tab.targetId === targetId);
			if (!exactTarget) throw new BrowserTabNotFoundError({ input: targetId });
			return exactTarget.targetId;
		}
		const resolved = resolveTargetIdFromTabs(targetId, tabs);
		if (!resolved.ok) {
			if (resolved.reason === "ambiguous") throw new BrowserTargetAmbiguousError();
			throw new BrowserTabNotFoundError({ input: targetId });
		}
		return resolved.targetId;
	};
	const focusTab = async (targetId, options) => {
		const resolvedTargetId = await resolveTargetIdOrThrow(targetId, options);
		if (capabilities.usesChromeMcp) {
			assertChromeMcpCdpTransportAllowed(profile, getCdpControlPolicy());
			const { focusChromeMcpTab } = await getChromeMcpModule();
			options?.signal?.throwIfAborted();
			await focusChromeMcpTab(profile.name, resolvedTargetId, profile, options);
			runtime.lastTargetId = resolvedTargetId;
			return;
		}
		if (capabilities.usesPersistentPlaywright) {
			const focusPageByTargetIdViaPlaywright = (await getPwAiModule({ mode: "strict" }))?.focusPageByTargetIdViaPlaywright;
			if (typeof focusPageByTargetIdViaPlaywright === "function") {
				options?.signal?.throwIfAborted();
				await focusPageByTargetIdViaPlaywright({
					cdpUrl: profile.cdpUrl,
					targetId: resolvedTargetId,
					ssrfPolicy: getCdpControlPolicy(),
					...options?.signal ? { signal: options.signal } : {}
				});
				runtime.lastTargetId = resolvedTargetId;
				return;
			}
		}
		options?.signal?.throwIfAborted();
		await fetchOk(appendCdpPath(cdpHttpBase, `/json/activate/${resolvedTargetId}`), void 0, options?.signal ? { signal: options.signal } : void 0, getCdpControlPolicy());
		runtime.lastTargetId = resolvedTargetId;
	};
	const closeTab = async (targetId, options) => {
		const resolvedTargetId = await resolveTargetIdOrThrow(targetId, options);
		if (capabilities.usesChromeMcp) {
			assertChromeMcpCdpTransportAllowed(profile, getCdpControlPolicy());
			const { closeChromeMcpTab } = await getChromeMcpModule();
			options?.signal?.throwIfAborted();
			await closeChromeMcpTab(profile.name, resolvedTargetId, profile, options);
		} else {
			let closedViaPlaywright = false;
			if (capabilities.usesPersistentPlaywright) {
				const closePageByTargetIdViaPlaywright = (await getPwAiModule({ mode: "strict" }))?.closePageByTargetIdViaPlaywright;
				if (typeof closePageByTargetIdViaPlaywright === "function") {
					options?.signal?.throwIfAborted();
					await closePageByTargetIdViaPlaywright({
						cdpUrl: profile.cdpUrl,
						targetId: resolvedTargetId,
						ssrfPolicy: getCdpControlPolicy(),
						...options?.signal ? { signal: options.signal } : {}
					});
					closedViaPlaywright = true;
				}
			}
			if (!closedViaPlaywright) {
				options?.signal?.throwIfAborted();
				await fetchOk(appendCdpPath(cdpHttpBase, `/json/close/${resolvedTargetId}`), void 0, options?.signal ? { signal: options.signal } : void 0, getCdpControlPolicy());
			}
		}
		if (runtime.lastTargetId === resolvedTargetId) runtime.lastTargetId = null;
		return resolvedTargetId;
	};
	return {
		ensureTabAvailable,
		focusTab,
		closeTab
	};
}
//#endregion
//#region extensions/browser/src/browser/cdp-target-filter.ts
/**
* CDP target filtering helpers.
*
* Browser-internal pages cannot be reliably automated as user content, so tab
* selection filters them before exposing targets to browser actions.
*/
const BROWSER_INTERNAL_TARGET_URL_PREFIXES = [
	"chrome://",
	"chrome-untrusted://",
	"devtools://",
	"edge://",
	"brave://",
	"vivaldi://",
	"opera://"
];
/** Return true for browser-owned chrome/devtools/internal URLs. */
function isBrowserInternalTargetUrl(url) {
	const normalized = url?.trim().toLowerCase() ?? "";
	return BROWSER_INTERNAL_TARGET_URL_PREFIXES.some((prefix) => normalized.startsWith(prefix));
}
/** Return true when a CDP page target should be selectable by user-facing actions. */
function isSelectableCdpBrowserTarget(target) {
	return target.type === "page" && !isBrowserInternalTargetUrl(target.url);
}
//#endregion
//#region extensions/browser/src/browser/server-context.tab-ops.ts
/**
* Browser tab listing, opening, labeling, and alias management for one profile.
*/
/** Normalize a reported CDP WebSocket URL against the configured endpoint. */
function normalizeWsUrl(raw, cdpBaseUrl) {
	if (!raw) return;
	try {
		return normalizeCdpWsUrl(raw, cdpBaseUrl);
	} catch {
		return raw;
	}
}
/** Builds list/open/label tab operations for one resolved browser profile. */
function createProfileTabOps({ profile, state, runtime }) {
	const cdpHttpBase = normalizeCdpHttpBaseForJsonEndpoints(profile.cdpUrl);
	const capabilities = getBrowserProfileCapabilities(profile);
	const getCdpControlPolicy = () => resolveCdpControlPolicy(profile, state().resolved.ssrfPolicy);
	const getNavigationPolicy = () => withBrowserNavigationPolicy(state().resolved.ssrfPolicy, { browserProxyMode: resolveBrowserNavigationProxyMode({
		resolved: state().resolved,
		profile
	}) });
	const getRemoteCdpActionTimeouts = () => {
		if (profile.cdpIsLoopback && !profile.attachOnly) return;
		const resolved = state().resolved;
		return {
			httpTimeoutMs: resolved.remoteCdpTimeoutMs,
			handshakeTimeoutMs: resolved.remoteCdpHandshakeTimeoutMs
		};
	};
	const readTabs = async (options) => {
		if (capabilities.usesChromeMcp) {
			assertChromeMcpCdpTransportAllowed(profile, getCdpControlPolicy());
			const { listChromeMcpTabs } = await getChromeMcpModule();
			return await listChromeMcpTabs(profile.name, profile, options);
		}
		if (capabilities.usesPersistentPlaywright) {
			const listPagesViaPlaywright = (await getPwAiModule({ mode: "strict" }))?.listPagesViaPlaywright;
			if (typeof listPagesViaPlaywright === "function") {
				const ssrfPolicy = getCdpControlPolicy();
				const resolved = state().resolved;
				const enumerationBudgetMs = typeof options?.timeoutMs === "number" && Number.isFinite(options.timeoutMs) ? options.timeoutMs : resolved.actionTimeoutMs;
				const timeoutMs = Math.max(enumerationBudgetMs, resolved.remoteCdpHandshakeTimeoutMs);
				await assertCdpEndpointAllowed(profile.cdpUrl, ssrfPolicy);
				return (await listPagesViaPlaywright({
					cdpUrl: profile.cdpUrl,
					ssrfPolicy,
					timeoutMs,
					...capabilities.requiresCompleteTargetEnumeration ? { requireCompleteTargetList: true } : {},
					...options?.signal ? { signal: options.signal } : {}
				})).filter(isSelectableCdpBrowserTarget).map((p) => ({
					targetId: p.targetId,
					title: p.title,
					url: p.url,
					type: p.type
				}));
			}
		}
		const raw = await fetchJson(appendCdpPath(cdpHttpBase, "/json/list"), void 0, void 0, getCdpControlPolicy());
		const cdpControlPolicy = getCdpControlPolicy();
		const tabs = [];
		for (const t of raw) {
			const tab = {
				targetId: t.id ?? "",
				title: t.title ?? "",
				url: t.url ?? "",
				wsUrl: normalizeWsUrl(t.webSocketDebuggerUrl, profile.cdpUrl),
				type: t.type
			};
			if (!tab.targetId || !isSelectableCdpBrowserTarget(tab)) continue;
			if (tab.wsUrl) {
				const wsPin = await assertCdpEndpointAllowed(tab.wsUrl, cdpControlPolicy, {
					source: "discovered",
					configuredUrl: profile.cdpUrl
				});
				if (wsPin?.lookup) tab.wsLookup = wsPin.lookup;
			}
			tabs.push(tab);
		}
		return tabs;
	};
	const listTabs = async (options) => {
		return assignTabAliases(runtime, await readTabs(options), !capabilities.usesChromeMcp);
	};
	const enforceManagedTabLimit = async (keepTargetId, options) => {
		if (!capabilities.supportsManagedTabLimit || state().resolved.attachOnly || !runtime.running) return;
		const pageTabs = await listTabs(options).then((tabs) => tabs.filter((tab) => (tab.type ?? "page") === "page")).catch(() => []);
		if (pageTabs.length <= 8) return;
		const candidates = pageTabs.filter((tab) => tab.targetId !== keepTargetId);
		const excessCount = pageTabs.length - 8;
		for (const tab of candidates.slice(0, excessCount)) {
			options?.signal?.throwIfAborted();
			await fetchOk(appendCdpPath(cdpHttpBase, `/json/close/${tab.targetId}`), void 0, void 0, getCdpControlPolicy()).catch(() => {});
		}
	};
	const triggerManagedTabLimit = (keepTargetId, options) => {
		enforceManagedTabLimit(keepTargetId, options).catch(() => {});
	};
	const adoptValidatedTab = (tab, options) => {
		options?.signal?.throwIfAborted();
		const adopted = assignTabAlias({
			profileState: runtime,
			tab,
			label: options?.label
		});
		runtime.lastTargetId = tab.targetId;
		triggerManagedTabLimit(tab.targetId, options);
		return adopted;
	};
	const withTabOwnership = async (tab, options) => {
		const cdpTimeouts = getRemoteCdpActionTimeouts();
		return {
			...tab,
			ownership: await resolveCdpTabOwnership({
				profileName: profile.name,
				cdpUrl: profile.cdpUrl,
				nativeTargetId: tab.targetId,
				signal: options?.signal,
				timeoutMs: cdpTimeouts?.httpTimeoutMs,
				ssrfPolicy: getCdpControlPolicy()
			})
		};
	};
	const openTab = async (url, opts) => {
		opts?.signal?.throwIfAborted();
		const normalizedLabel = opts?.label === void 0 ? void 0 : normalizeTabLabel(opts.label);
		const ssrfPolicyOpts = getNavigationPolicy();
		const cdpPolicy = getCdpControlPolicy();
		const cleanupTimeoutMs = state().resolved.remoteCdpTimeoutMs;
		if (capabilities.usesChromeMcp) {
			await assertBrowserNavigationAllowed({
				url,
				...ssrfPolicyOpts
			});
			assertChromeMcpCdpTransportAllowed(profile, cdpPolicy);
			const { openChromeMcpTab } = await getChromeMcpModule();
			const cdpTimeouts = getRemoteCdpActionTimeouts();
			const page = await openChromeMcpTab(profile.name, url, profile, {
				signal: opts?.signal,
				timeoutMs: opts?.timeoutMs,
				cdpPolicy,
				...cdpTimeouts ? { cdpTimeouts } : {}
			});
			await assertBrowserNavigationResultAllowed({
				url: page.url,
				...ssrfPolicyOpts
			});
			return adoptValidatedTab(page, {
				...opts,
				label: normalizedLabel
			});
		}
		let createdTargetId;
		try {
			if (capabilities.usesPersistentPlaywright) {
				const createPageViaPlaywright = (await getPwAiModule({ mode: "strict" }))?.createPageViaPlaywright;
				if (typeof createPageViaPlaywright === "function") {
					const page = await createPageViaPlaywright({
						cdpUrl: profile.cdpUrl,
						url,
						cdpPolicy,
						...opts?.signal ? { signal: opts.signal } : {},
						...ssrfPolicyOpts
					});
					createdTargetId = page.targetId;
					return adoptValidatedTab(await withTabOwnership({
						targetId: page.targetId,
						title: page.title,
						url: page.url,
						type: page.type
					}, opts), {
						...opts,
						label: normalizedLabel
					});
				}
			}
			if (requiresInspectableBrowserNavigationRedirectsForUrl(url, state().resolved.ssrfPolicy)) throw new InvalidBrowserNavigationUrlError("Navigation blocked: strict browser SSRF policy requires Playwright-backed redirect-hop inspection");
			await assertBrowserNavigationAllowed({
				url,
				...ssrfPolicyOpts
			});
			const cdpActionTimeouts = getRemoteCdpActionTimeouts();
			const createTargetOpts = {
				cdpUrl: profile.cdpUrl,
				url,
				ssrfPolicy: cdpPolicy,
				waitForNavigationResult: true
			};
			if (cdpActionTimeouts) createTargetOpts.timeouts = cdpActionTimeouts;
			if (opts?.signal) createTargetOpts.signal = opts.signal;
			const createdViaCdp = await createTargetViaCdp(createTargetOpts).catch(() => null);
			createdTargetId = createdViaCdp?.targetId;
			opts?.signal?.throwIfAborted();
			if (createdViaCdp) {
				if (!createdViaCdp.finalUrl) return await withTabOwnership({
					targetId: createdViaCdp.targetId,
					title: "",
					url,
					type: "page"
				}, opts);
				await assertBrowserNavigationResultAllowed({
					url: createdViaCdp.finalUrl,
					...ssrfPolicyOpts
				});
				const deadline = Date.now() + OPEN_TAB_DISCOVERY_WINDOW_MS;
				while (Date.now() < deadline) {
					opts?.signal?.throwIfAborted();
					const found = (await readTabs(opts).catch(() => [])).find((t) => t.targetId === createdViaCdp.targetId);
					if (found) {
						await assertBrowserNavigationResultAllowed({
							url: found.url,
							...ssrfPolicyOpts
						});
						return adoptValidatedTab(await withTabOwnership({
							...found,
							url: createdViaCdp.finalUrl
						}, opts), {
							...opts,
							label: normalizedLabel
						});
					}
					await sleepWithAbort(100, opts?.signal);
				}
				opts?.signal?.throwIfAborted();
				return await withTabOwnership({
					targetId: createdViaCdp.targetId,
					title: "",
					url: createdViaCdp.finalUrl,
					type: "page"
				}, opts);
			}
			const encoded = encodeURIComponent(url);
			const endpointUrl = new URL(appendCdpPath(cdpHttpBase, "/json/new"));
			const endpoint = endpointUrl.search ? (() => {
				endpointUrl.searchParams.set("url", url);
				return endpointUrl.toString();
			})() : `${endpointUrl.toString()}?${encoded}`;
			opts?.signal?.throwIfAborted();
			const created = await fetchJson(endpoint, cdpActionTimeouts?.httpTimeoutMs ?? 1500, { method: "PUT" }, getCdpControlPolicy()).catch(async (err) => {
				if (String(err).includes("HTTP 405")) return await fetchJson(endpoint, cdpActionTimeouts?.httpTimeoutMs ?? 1500, void 0, getCdpControlPolicy());
				throw err;
			});
			createdTargetId = created.id;
			opts?.signal?.throwIfAborted();
			if (!created.id) throw new Error("Failed to open tab (missing id)");
			const resolvedUrl = created.url ?? url;
			if (!isSelectableCdpBrowserTarget({
				url: resolvedUrl,
				type: created.type
			})) throw new Error("Failed to open tab (non-selectable target)");
			await assertBrowserNavigationResultAllowed({
				url: resolvedUrl,
				...ssrfPolicyOpts
			});
			const wsUrl = normalizeWsUrl(created.webSocketDebuggerUrl, profile.cdpUrl);
			const wsPin = wsUrl ? await assertCdpEndpointAllowed(wsUrl, getCdpControlPolicy(), {
				source: "discovered",
				configuredUrl: profile.cdpUrl
			}) : void 0;
			const committedUrl = wsUrl ? await waitForCdpCommittedNavigationUrl({
				wsUrl,
				configuredCdpUrl: profile.cdpUrl,
				cdpPolicy: getCdpControlPolicy(),
				requestedUrl: url,
				signal: opts?.signal,
				timeouts: cdpActionTimeouts
			}) : void 0;
			opts?.signal?.throwIfAborted();
			if (!committedUrl) return await withTabOwnership({
				targetId: created.id,
				title: created.title ?? "",
				url: resolvedUrl,
				wsUrl,
				...wsPin?.lookup ? { wsLookup: wsPin.lookup } : {},
				type: created.type
			}, opts);
			await assertBrowserNavigationResultAllowed({
				url: committedUrl,
				...ssrfPolicyOpts
			});
			return adoptValidatedTab(await withTabOwnership({
				targetId: created.id,
				title: created.title ?? "",
				url: committedUrl,
				wsUrl,
				...wsPin?.lookup ? { wsLookup: wsPin.lookup } : {},
				type: created.type
			}, opts), {
				...opts,
				label: normalizedLabel
			});
		} catch (openError) {
			if (createdTargetId) await fetchOk(appendCdpPath(cdpHttpBase, `/json/close/${encodeURIComponent(createdTargetId)}`), cleanupTimeoutMs, void 0, cdpPolicy).catch(() => {});
			throw openError;
		}
	};
	const labelTab = async (targetId, label, options) => {
		const normalizedLabel = normalizeTabLabel(label);
		const tabs = await listTabs(options);
		const resolved = resolveTargetIdFromTabs(targetId, tabs);
		if (!resolved.ok) {
			if (resolved.reason === "ambiguous") throw new BrowserTargetAmbiguousError();
			throw new BrowserTabNotFoundError({ input: targetId });
		}
		const tab = tabs.find((candidate) => candidate.targetId === resolved.targetId);
		if (!tab) throw new BrowserTabNotFoundError({ input: targetId });
		return assignTabAlias({
			profileState: runtime,
			tab,
			label: normalizedLabel
		});
	};
	return {
		listTabs,
		openTab,
		labelTab
	};
}
//#endregion
//#region extensions/browser/src/browser/server-context.ts
/**
* Browser route context factory that wires profile-scoped runtime operations for
* the Browser control server.
*/
/** Lists configured and runtime-known Browser profile names without duplicates. */
function listKnownProfileNames(state) {
	const names = new Set(Object.keys(state.resolved.profiles));
	for (const name of state.profiles.keys()) names.add(name);
	return [...names];
}
const profileOperationRunners = /* @__PURE__ */ new WeakMap();
/** Internal actor lease entrypoint; not part of the public Browser runtime API. */
function runProfileContextOperation(profileCtx, signal, run, options) {
	const runner = profileOperationRunners.get(profileCtx);
	if (!runner) throw new BrowserProfileUnavailableError("Browser profile context is no longer active.");
	return runner(signal, run, options);
}
/** Preserve custom route contexts while leasing contexts created by this runtime. */
function withProfileContextOperation(profileCtx, signal, run) {
	const runner = profileOperationRunners.get(profileCtx);
	if (!runner) return run(signal ?? new AbortController().signal);
	return runner(signal, async (leasedSignal) => await run(leasedSignal));
}
/**
* Create a profile-scoped context for browser operations.
*/
function createProfileContext(opts, runtimeState, profileState, profile) {
	const state = () => {
		if (opts.getState() !== runtimeState || !isBrowserRuntimeRunning(runtimeState)) throw new BrowserProfileUnavailableError("Browser runtime changed or is stopping.");
		return runtimeState;
	};
	const configRevision = getProfileLifecycle(profileState).configRevision;
	const rawTabOps = createProfileTabOps({
		profile,
		state,
		runtime: profileState
	});
	const rawAvailability = createProfileAvailability({
		opts,
		profile,
		state,
		runtime: profileState,
		configRevision
	});
	const rawSelection = createProfileSelectionOps({
		profile,
		runtime: profileState,
		getCdpControlPolicy: () => resolveCdpControlPolicy(profile, state().resolved.ssrfPolicy),
		ensureBrowserAvailable: rawAvailability.ensureBrowserAvailable,
		listTabs: rawTabOps.listTabs,
		openTab: rawTabOps.openTab
	});
	const rawReset = createProfileResetOps({
		profile,
		state,
		runtime: profileState,
		configRevision,
		resolveOpenClawUserDataDir
	});
	const withLease = async (callerSignal, run, options) => await withProfileOperationLease({
		state: state(),
		runtime: profileState,
		configRevision,
		signal: callerSignal,
		run: async (lifecycleSignal) => await run(lifecycleSignal, profileState),
		commit: options?.commit
	});
	const { ensureBrowserAvailable, stopRunningBrowser } = rawAvailability;
	const context = {
		profile,
		ensureBrowserAvailable,
		ensureTabAvailable: async (targetId, options) => {
			await ensureBrowserAvailable({ signal: options?.signal });
			return await withLease(options?.signal, async (signal) => await rawSelection.ensureTabAvailable(targetId, {
				...options,
				signal
			}, true));
		},
		isHttpReachable: async (timeoutMs, callerSignal) => await withLease(callerSignal, async (signal) => await rawAvailability.isHttpReachable(timeoutMs, signal)),
		isTransportAvailable: async (timeoutMs, callerSignal, pageProbe) => await withLease(callerSignal, async (signal) => await rawAvailability.isTransportAvailable(timeoutMs, signal, pageProbe)),
		isReachable: async (timeoutMs, options) => await withLease(options?.signal, async (signal) => await rawAvailability.isReachable(timeoutMs, {
			...options,
			signal
		})),
		listTabs: async (options) => await withLease(options?.signal, async (signal) => await rawTabOps.listTabs({
			...options,
			signal
		})),
		openTab: async (url, options) => await withLease(options?.signal, async (signal) => await rawTabOps.openTab(url, {
			...options,
			signal
		})),
		labelTab: async (targetId, label) => await withLease(void 0, async (signal) => await rawTabOps.labelTab(targetId, label, { signal })),
		focusTab: async (targetId, options) => await withLease(options?.signal, async (signal) => await rawSelection.focusTab(targetId, {
			...options,
			signal
		})),
		closeTab: async (targetId, options) => await withLease(options?.signal, async (signal) => await rawSelection.closeTab(targetId, {
			...options,
			signal
		})),
		stopRunningBrowser,
		resetProfile: rawReset.resetProfile
	};
	profileOperationRunners.set(context, withLease);
	return context;
}
/** Creates the Browser route context used by control-server route handlers. */
function createBrowserRouteContext(opts) {
	const refreshConfigFromDisk = opts.refreshConfigFromDisk === true;
	const state = () => {
		const current = opts.getState();
		if (!current) throw new BrowserProfileUnavailableError("Browser server not started.");
		if (!isBrowserRuntimeRunning(current)) throw new BrowserProfileUnavailableError("Browser runtime is stopping.");
		refreshResolvedBrowserConfigFromDisk({
			current,
			refreshConfigFromDisk
		});
		return current;
	};
	const forProfile = (profileName) => {
		const current = state();
		const name = profileName ?? current.resolved.defaultProfile;
		const profile = resolveProfile(current.resolved, name);
		if (!profile) {
			const available = Object.keys(current.resolved.profiles).join(", ");
			throw new BrowserProfileNotFoundError(`Profile "${name}" not found. Available profiles: ${available || "(none)"}`);
		}
		return createProfileContext(opts, current, getOrCreateProfileRuntime(current, profile), profile);
	};
	const listProfiles = async () => {
		const current = state();
		const result = [];
		for (const name of listKnownProfileNames(current)) {
			let profileState = current.profiles.get(name);
			const profile = resolveProfile(current.resolved, name) ?? profileState?.profile;
			if (!profile) continue;
			profileState ??= getOrCreateProfileRuntime(current, profile);
			let statusProfile = profile;
			let unavailableReason = null;
			let running = false;
			let tabCount = 0;
			for (let attempt = 0; attempt < 2; attempt += 1) {
				statusProfile = profileState.profile;
				const profileCtx = createProfileContext(opts, current, profileState, statusProfile);
				try {
					const snapshot = await runProfileContextOperation(profileCtx, void 0, async (signal, runtime) => {
						const activeProfile = runtime.profile;
						const capabilities = getBrowserProfileCapabilities(activeProfile);
						let activeRunning;
						let activeTabCount = 0;
						if (capabilities.usesChromeMcp) try {
							activeRunning = await profileCtx.isTransportAvailable(300, signal, { onResult: (observedTabCount) => activeTabCount = observedTabCount ?? 0 });
						} catch {
							activeRunning = false;
						}
						else if (runtime.running) {
							activeRunning = true;
							try {
								activeTabCount = (await profileCtx.listTabs({ signal })).filter((tab) => tab.type === "page").length;
							} catch {}
						} else try {
							const probeTimeoutMs = usesFastLoopbackCdpProbeClass({
								profileIsLoopback: activeProfile.cdpIsLoopback,
								attachOnly: activeProfile.attachOnly
							}) ? 200 : current.resolved.remoteCdpTimeoutMs;
							activeRunning = capabilities.mode === "local-extension" ? await profileCtx.isTransportAvailable(probeTimeoutMs, signal) : await isChromeReachable(activeProfile.cdpUrl, probeTimeoutMs, resolveCdpReachabilityPolicy(activeProfile, current.resolved.ssrfPolicy));
							if (activeRunning) activeTabCount = (await profileCtx.listTabs({ signal }).catch(() => [])).filter((tab) => tab.type === "page").length;
						} catch {
							activeRunning = false;
						}
						signal.throwIfAborted();
						return {
							profile: activeProfile,
							running: activeRunning,
							tabCount: activeTabCount
						};
					});
					statusProfile = snapshot.profile;
					running = snapshot.running;
					tabCount = snapshot.tabCount;
					break;
				} catch (err) {
					if (attempt === 0) continue;
					statusProfile = profileState.profile;
					const actor = getProfileLifecycle(profileState);
					unavailableReason = actor.blockedReason ?? actor.transitionReason ?? actor.terminal;
					if (!unavailableReason && !toBrowserErrorResponse(err)) throw err;
					running = Boolean(profileState.running);
					tabCount = 0;
				}
			}
			const capabilities = getBrowserProfileCapabilities(statusProfile);
			result.push({
				name,
				transport: capabilities.usesChromeMcp ? "chrome-mcp" : capabilities.mode === "local-extension" ? "extension" : "cdp",
				cdpPort: capabilities.usesChromeMcp ? null : statusProfile.cdpPort,
				cdpUrl: statusProfile.cdpUrl ? redactCdpUrl(statusProfile.cdpUrl) ?? null : null,
				color: statusProfile.color,
				driver: statusProfile.driver,
				running,
				tabCount,
				isDefault: name === current.resolved.defaultProfile,
				isRemote: !statusProfile.cdpIsLoopback,
				missingFromConfig: getOwnBrowserProfile(current.resolved.profiles, name) === void 0 || void 0,
				reconcileReason: unavailableReason
			});
		}
		return result;
	};
	const getDefaultContext = () => forProfile();
	const mapTabError = (err) => {
		const browserMapped = toBrowserErrorResponse(err);
		if (browserMapped) return browserMapped;
		return null;
	};
	return {
		state,
		forProfile,
		listProfiles,
		ensureBrowserAvailable: (options) => getDefaultContext().ensureBrowserAvailable(options),
		ensureTabAvailable: (targetId, options) => getDefaultContext().ensureTabAvailable(targetId, options),
		isHttpReachable: (timeoutMs) => getDefaultContext().isHttpReachable(timeoutMs),
		isTransportAvailable: (timeoutMs) => getDefaultContext().isTransportAvailable(timeoutMs),
		isReachable: (timeoutMs, options) => getDefaultContext().isReachable(timeoutMs, options),
		listTabs: () => getDefaultContext().listTabs(),
		openTab: (url, optsLocal) => getDefaultContext().openTab(url, optsLocal),
		labelTab: (targetId, label) => getDefaultContext().labelTab(targetId, label),
		focusTab: (targetId, options) => getDefaultContext().focusTab(targetId, options),
		closeTab: (targetId, options) => getDefaultContext().closeTab(targetId, options),
		stopRunningBrowser: () => getDefaultContext().stopRunningBrowser(),
		resetProfile: () => getDefaultContext().resetProfile(),
		mapTabError
	};
}
//#endregion
export { loadBrowserConfigForRuntimeRefresh as a, resolveTargetIdFromTabs as i, runProfileContextOperation as n, withProfileContextOperation as r, createBrowserRouteContext as t };