UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

2,534 lines • 101 kB
import { c as normalizeOptionalLowercaseString, g as readStringValue, l as normalizeOptionalString } from "./string-coerce-1VuiLaaR.mjs";
import { F as resolveTimerTimeoutMs, N as resolveOptionalIntegerOption, a as addTimerTimeoutGraceMs, m as clampTimerTimeoutMs, p as clampPositiveTimerTimeoutMs } from "./number-coercion-CkhMiRw8.mjs";
import { n as asNullableObjectRecord, o as asRecord, r as asNullableRecord } from "./record-coerce-TIsquoVR.mjs";
import { r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.mjs";
import { t as formatErrorMessage } from "./errors-BvOkmQM5.mjs";
import { t as createSubsystemLogger } from "./subsystem-4NzR43uz.mjs";
import { Et as _enum, Fn as object, Jn as string, Lt as boolean, Nt as array, qn as strictObject } from "./schemas-BOYIvvln.mjs";
import { r as getRuntimeConfig } from "./io.runtime-B134MOMz.mjs";
import { t as ErrorCodes } from "./gateway-error-details-gTekQWj2.mjs";
import { d as errorShape } from "./error-codes-D0WRV4Bq.mjs";
import { n as estimateBase64DecodedBytes, t as canonicalizeBase64 } from "./base64-CU53EX1R.mjs";
import { t as DEFAULT_MAX_LIVE_TOOL_RESULT_CHARS } from "./tool-result-limits-DNBSu_jg.mjs";
import { a as wrapExternalContent, i as truncateSanitizedExternalContent } from "./external-content-Bkm4w5qo.mjs";
import "./string-coerce-runtime-BbIJhy7G.mjs";
import "./security-runtime-DBKL3aSe.mjs";
import "./text-utility-runtime-BB0hWeEg.mjs";
import { n as textResult, t as jsonResult } from "./tool-results-BCM3fdVS.mjs";
import { c as readNonNegativeIntegerParam, m as readToolStringParam, r as imageResultFromFile, u as readPositiveIntegerParam } from "./common-BkU19q6Q.mjs";
import { f as saveMediaBuffer } from "./store-7jnF8fA5.mjs";
import { r as readGatewayToolOperatorScopes, t as callGatewayTool } from "./gateway-1wftaKTM.mjs";
import "./runtime-env-CySdLHPX.mjs";
import "./number-runtime-D3v2w3D4.mjs";
import { o as hasGatewayToolRoutingContext } from "./in-process-gateway-BYRZzLBf.mjs";
import { l as resolveNodeCommandAllowlist, o as isNodeCommandAllowed } from "./node-command-policy-I3ly1VaT.mjs";
import { n as listNodes, o as resolveNodeIdFromList } from "./nodes-utils-7O6FcwEo.mjs";
import "./runtime-config-snapshot--IkldAgP.mjs";
import "./media-runtime-Bn5-G6uL.mjs";
import "./media-store-BbouPwWH.mjs";
import { r as parseGatewayPayload, t as respondUnavailableOnNodeInvokeError } from "./nodes.helpers-UXSISi6G.mjs";
import "./gateway-runtime-hhusjSIO.mjs";
import "./agent-harness-runtime-C1yVhaNg.mjs";
import { a as createDeferred } from "./extension-shared-Df_UV_E_.mjs";
import "./channel-actions-CZEXLh24.mjs";
import "./param-readers-2yybHely.mjs";
import { J as getOptionalBrowserStateRuntime, L as resolveBrowserSessionKey, W as parseBrowserSessionTabCloseResult } from "./session-tab-store-CqcKOTbk.mjs";
import { a as browserProxyUploadUnavailableMessage, i as BROWSER_PROXY_UPLOAD_COMMAND, n as applyBrowserTabToolBinding, r as BROWSER_PROXY_COMMAND, t as createBrowserToolDefinition } from "./browser-tool-description-BPI9dH7G.mjs";
import { i as DEFAULT_BROWSER_ACTION_TIMEOUT_MS, r as DEFAULT_AI_SNAPSHOT_MAX_CHARS } from "./constants-CxIE9Xhk.mjs";
import { d as resolveBrowserNavigationTimeoutMs, o as EXISTING_SESSION_TIMEOUT_OVERRIDE_KINDS, u as resolveBrowserActRequestTimeoutMs } from "./act-policy-BRluzhGf.mjs";
import { i as resolveBrowserConfig, s as resolveProfile } from "./config-NGx7eSU6.mjs";
import { t as getBrowserProfileCapabilities } from "./profile-capabilities-DXuiwiC6.mjs";
import { E as findRoleSnapshotLineRef, L as parseBrowserNavigationUrl, T as finalizeRoleSnapshot } from "./chrome-IDR7c0-o.mjs";
import { i as resolveExistingUploadPaths } from "./paths-Dxrj5C-I.mjs";
import { a as trackSessionBrowserTab, i as touchSessionBrowserTab, o as untrackSessionBrowserTab, t as filterTrackedSessionBrowserTabs } from "./session-tab-tracking-BG9zVN_r.mjs";
import { a as normalizeBrowserRequestPath, i as isPersistentBrowserProfileMutation, o as resolveRequestedBrowserProfile, r as isBrowserHostLocalRoute } from "./routes-CKmbYgHR.mjs";
import { r as normalizeBrowserScreenshot, t as DEFAULT_BROWSER_SCREENSHOT_MAX_BYTES } from "./screenshot-CwWza9FB.mjs";
import { t as parseSystemProfileDomains } from "./system-profile-domains-BWFYn1am.mjs";
import { t as createBrowserRouteDispatcher } from "./dispatcher-CMmeKAKA.mjs";
import { r as session_tab_registry_exports } from "./session-tab-registry-CaPXI6ju.mjs";
import { a as prepareBrowserProxyUploadRequest, c as BROWSER_PROXY_MAX_FILE_BYTES, d as assertBrowserProxyFileCountWithinLimit, h as visitBrowserProxyFilePaths, i as isBrowserProxyUploadRequest, l as BROWSER_PROXY_OWNED_TAB_CLOSE_PATH, m as parseBrowserProxyRoute, p as parseBrowserProxyFailure, s as BROWSER_PROXY_ERROR_ENVELOPE, u as assertBrowserProxyFileBytesWithinLimits } from "./browser-proxy-upload-C7NupnOO.mjs";
import { n as resolveBrowserProxyTimeouts } from "./browser-proxy-timeouts-C7OWnqcB.mjs";
import { a as fetchBrowserJson, i as BrowserServiceError, n as postBrowserJson, o as withBrowserRequestScope, r as requestBrowserJson, t as browserClientTimeout } from "./client-request-BprwEKvQ.mjs";
import { a as browserOpenTab, c as browserStart, d as browserSystemProfiles, f as browserTabs, i as browserImportProfile, l as browserStatus, n as browserDoctor, o as browserProfiles, r as browserFocusTab, s as browserSnapshot, t as browserCloseTab, u as browserStop } from "./client-Dc0lFCyl.mjs";
import { t as createBrowserControlContext } from "./browser-control-state-C71zi1IJ.mjs";
import { t as describeBrowserControlUnavailable } from "./plugin-enabled-CAWE2HwB.mjs";
import { t as startBrowserControlServiceFromConfig } from "./control-service-B7K63rCu.mjs";
import path from "node:path";
import { readFile } from "node:fs/promises";
import crypto from "node:crypto";
//#region extensions/browser/src/browser-node-fallback.ts
/**
* Only the node host's explicit pre-dispatch reachability failure is safe to
* retry on the Gateway host. Other failures may follow a mutating action.
*/
const BROWSER_CONTROL_HOST_UNREACHABLE = /\bbrowser control host is not reachable\b/i;
function isBrowserControlHostUnavailableError(value) {
	const seen = /* @__PURE__ */ new Set();
	const visit = (candidate, depth) => {
		if (typeof candidate === "string") return BROWSER_CONTROL_HOST_UNREACHABLE.test(candidate);
		if (!candidate || typeof candidate !== "object" || depth > 3 || seen.has(candidate)) return false;
		seen.add(candidate);
		const record = candidate;
		if (typeof record.message === "string" && BROWSER_CONTROL_HOST_UNREACHABLE.test(record.message)) return true;
		return [
			record.error,
			record.cause,
			record.details,
			record.nodeError
		].some((entry) => visit(entry, depth + 1));
	};
	return visit(value, 0);
}
//#endregion
//#region extensions/browser/src/browser/proxy-files.ts
const INVALID_BROWSER_PROXY_FILE_ENVELOPE = "browser proxy returned an invalid file envelope";
function invalidBrowserProxyFileEnvelope() {
	throw new Error(INVALID_BROWSER_PROXY_FILE_ENVELOPE);
}
function collectBrowserProxyResultPaths(result) {
	const paths = /* @__PURE__ */ new Set();
	visitBrowserProxyFilePaths(result, (filePath) => {
		paths.add(filePath);
		assertBrowserProxyFileCountWithinLimit(paths.size);
	});
	return paths;
}
function validateBrowserProxyFiles(result, files) {
	const referencedPaths = collectBrowserProxyResultPaths(result);
	const candidates = files === void 0 ? [] : files;
	if (!Array.isArray(candidates)) return invalidBrowserProxyFileEnvelope();
	assertBrowserProxyFileCountWithinLimit(candidates.length);
	const validated = [];
	for (const value of candidates) {
		const file = asNullableRecord(value);
		if (!file || typeof file.path !== "string" || !file.path.trim() || typeof file.base64 !== "string" || file.mimeType !== void 0 && typeof file.mimeType !== "string" || !referencedPaths.delete(file.path)) return invalidBrowserProxyFileEnvelope();
		validated.push({
			path: file.path,
			base64: file.base64,
			...file.mimeType === void 0 ? {} : { mimeType: file.mimeType }
		});
	}
	if (referencedPaths.size > 0) return invalidBrowserProxyFileEnvelope();
	return validated;
}
function decodeBrowserProxyFileBase64(file, totalBytes) {
	const estimatedBytes = estimateBase64DecodedBytes(file.base64);
	assertBrowserProxyFileBytesWithinLimits(estimatedBytes, totalBytes + estimatedBytes);
	const canonicalBase64 = file.base64 === "" ? "" : canonicalizeBase64(file.base64);
	if (canonicalBase64 === void 0) throw new Error("browser proxy file contains malformed base64 data");
	const buffer = Buffer.from(canonicalBase64, "base64");
	assertBrowserProxyFileBytesWithinLimits(buffer.byteLength, totalBytes + buffer.byteLength);
	return buffer;
}
/** Validate, persist, and rewrite every route-owned file in a node result. */
async function persistBrowserProxyResultFiles(result, files) {
	const validatedFiles = validateBrowserProxyFiles(result, files);
	if (validatedFiles.length === 0) return result;
	const decoded = [];
	let totalBytes = 0;
	for (const file of validatedFiles) {
		const buffer = decodeBrowserProxyFileBase64(file, totalBytes);
		totalBytes += buffer.byteLength;
		decoded.push({
			file,
			buffer
		});
	}
	const mapping = /* @__PURE__ */ new Map();
	for (const { file, buffer } of decoded) {
		const saved = await saveMediaBuffer(buffer, file.mimeType, "browser", BROWSER_PROXY_MAX_FILE_BYTES, file.path);
		mapping.set(file.path, saved.path);
	}
	visitBrowserProxyFilePaths(result, (filePath) => mapping.get(filePath));
	return result;
}
//#endregion
//#region extensions/browser/src/browser-node-proxy.ts
const logger$1 = createSubsystemLogger("browser");
var BrowserNodeSafeFallbackError = class extends Error {
	constructor(message, cause) {
		super(message, cause === void 0 ? void 0 : { cause });
		this.name = "BrowserNodeSafeFallbackError";
	}
};
function unwrapBrowserProxyPayload(payload) {
	if (payload?.payload !== void 0) return payload.payload;
	if (typeof payload?.payloadJSON !== "string" || !payload.payloadJSON.trim()) return null;
	try {
		return JSON.parse(payload.payloadJSON);
	} catch {
		return null;
	}
}
async function callBrowserProxy(params) {
	const { proxyTimeoutMs, nodeInvokeTimeoutMs, gatewayTimeoutMs } = resolveBrowserProxyTimeouts(params.timeoutMs);
	if (isBrowserProxyUploadRequest(params) && !params.declaredCommands.includes("browser.proxy.upload.v1")) throw new BrowserNodeSafeFallbackError(browserProxyUploadUnavailableMessage(params.pendingDeclaredCommands));
	const preparedUpload = await prepareBrowserProxyUploadRequest({
		method: params.method,
		path: params.path,
		body: params.body,
		signal: params.signal
	});
	const command = preparedUpload.upload ? BROWSER_PROXY_UPLOAD_COMMAND : BROWSER_PROXY_COMMAND;
	let payload;
	try {
		payload = await callGatewayTool("node.invoke", { timeoutMs: gatewayTimeoutMs }, {
			nodeId: params.nodeId,
			command,
			timeoutMs: nodeInvokeTimeoutMs,
			params: {
				method: params.method,
				path: params.path,
				query: params.query,
				body: preparedUpload.body,
				upload: preparedUpload.upload,
				timeoutMs: proxyTimeoutMs,
				profile: params.profile,
				errorEnvelope: BROWSER_PROXY_ERROR_ENVELOPE
			},
			idempotencyKey: crypto.randomUUID()
		}, {
			scopes: ["operator.admin"],
			...params.signal ? { signal: params.signal } : {}
		});
	} catch (error) {
		if (params.allowAutomaticHostFallback && isBrowserControlHostUnavailableError(error)) throw new BrowserNodeSafeFallbackError("browser node control host unavailable", error);
		throw error;
	}
	const parsed = unwrapBrowserProxyPayload(payload);
	if (!parsed || typeof parsed !== "object" || !("result" in parsed) && !parseBrowserProxyFailure(parsed)) {
		const selectedNode = truncateUtf16Safe(params.nodeLabel?.trim() || params.nodeId, 256);
		throw new Error(`Browser proxy returned an invalid response from node ${JSON.stringify(selectedNode)}. Retry with action=status target="host" to check Gateway host browser control.`);
	}
	return parsed;
}
async function callLocalBrowserControl(params) {
	const url = new URL(params.path, "http://localhost");
	for (const [key, value] of Object.entries(params.query ?? {})) if (value !== void 0) url.searchParams.set(key, String(value));
	if (params.profile) url.searchParams.set("profile", params.profile);
	return await fetchBrowserJson(`${url.pathname}${url.search}`, {
		method: params.method,
		body: params.body === void 0 ? void 0 : JSON.stringify(params.body),
		timeoutMs: params.timeoutMs,
		signal: params.signal
	});
}
function createBrowserNodeProxyRequest(params) {
	let target = params.allowAutomaticHostFallback ? "auto" : "node";
	let route;
	const dispatch = async (request) => {
		const requestWithSignal = request.signal || params.signal ? {
			...request,
			signal: request.signal ?? params.signal
		} : request;
		if (target === "host") return await callLocalBrowserControl(requestWithSignal);
		try {
			const proxy = await callBrowserProxy({
				nodeId: params.nodeTarget.nodeId,
				nodeLabel: params.nodeTarget.label,
				declaredCommands: params.nodeTarget.commands ?? [],
				pendingDeclaredCommands: params.nodeTarget.pendingDeclaredCommands ?? [],
				allowAutomaticHostFallback: target === "auto",
				...requestWithSignal
			});
			target = "node";
			route = parseBrowserProxyRoute(proxy);
			const failure = parseBrowserProxyFailure(proxy);
			if (failure) {
				const { status, body } = failure.error;
				throw new BrowserServiceError(body.error, body, status);
			}
			if (!("result" in proxy)) throw new Error("Browser proxy returned a failure without an error payload.");
			return await persistBrowserProxyResultFiles(proxy.result, proxy.files);
		} catch (error) {
			if (target !== "auto" || !(error instanceof BrowserNodeSafeFallbackError)) throw error;
			target = "host";
			route = void 0;
			logger$1.warn(`browser node ${params.nodeTarget.label ?? params.nodeTarget.nodeId} unavailable before dispatch (${error.message}); falling back to Gateway host`);
			return await callLocalBrowserControl(requestWithSignal);
		}
	};
	return Object.assign(dispatch, {
		isHostFallbackActive: () => target === "host",
		route: () => route
	});
}
function createBrowserNodeSessionTabRoute(nodeTarget) {
	return {
		kind: "node-proxy",
		nodeId: nodeTarget.nodeId,
		closeTarget: async (tab) => {
			const cleanupProxy = createBrowserNodeProxyRequest({
				nodeTarget,
				allowAutomaticHostFallback: false
			});
			if (tab.ownership?.status === "durable") return parseBrowserSessionTabCloseResult(await cleanupProxy({
				method: "POST",
				path: BROWSER_PROXY_OWNED_TAB_CLOSE_PATH,
				body: { ownership: tab.ownership },
				profile: tab.profile
			}));
			await cleanupProxy({
				method: "DELETE",
				path: `/tabs/${encodeURIComponent(tab.targetId)}`,
				query: { targetIdMode: "raw" },
				profile: tab.profile
			});
			return { status: "closed" };
		}
	};
}
//#endregion
//#region extensions/browser/src/browser-tool-session-tabs.ts
function readOpenedTab(result) {
	const opened = asNullableRecord(result);
	if (!opened) return { aliases: [] };
	const targetId = normalizeOptionalString(opened.targetId);
	const aliases = [
		targetId,
		normalizeOptionalString(opened.tabId),
		normalizeOptionalString(opened.label),
		normalizeOptionalString(opened.suggestedTargetId)
	].filter((alias) => Boolean(alias));
	const profile = normalizeOptionalString(opened.resolvedProfile);
	const rawOwnership = opened.ownership && typeof opened.ownership === "object" ? opened.ownership : void 0;
	const ownership = rawOwnership?.status === "durable" && !profile ? void 0 : rawOwnership;
	return {
		targetId,
		aliases: [...new Set(aliases)],
		profile,
		ownership
	};
}
function stripBrowserOpenInternalMetadata(value) {
	const record = asNullableRecord(value);
	if (!record) return value;
	const { ownership: _ownership, resolvedProfile: _resolvedProfile, ...agentVisible } = record;
	return agentVisible;
}
function createBrowserToolSessionTabs(params) {
	const sessionKey = resolveBrowserSessionKey(params.sessionKey, params.agentId);
	if (params.sessionKey?.trim() && !sessionKey) throw new Error("Browser tab ownership requires an agent-qualified session key or an explicit agent id.");
	const trackedRoute = () => params.nodeRoute && !params.isHostFallbackActive?.() ? params.nodeRoute : {
		kind: "browser-control",
		...params.baseUrl ? { baseUrl: params.baseUrl } : {}
	};
	const trackedProfile = (route) => route.kind === "node-proxy" ? params.routeProfile?.() ?? params.requestedProfile : route.baseUrl && !params.requestedProfile ? void 0 : params.requestedProfile ?? params.defaultProfile;
	const identity = (targetId) => {
		const route = trackedRoute();
		return {
			sessionKey,
			targetId,
			route,
			profile: trackedProfile(route),
			...params.authority ? { authority: params.authority } : {}
		};
	};
	return {
		touch: async (targetId) => {
			if (targetId) await params.registry.touchSessionBrowserTab(identity(targetId));
		},
		untrack: async (targetId) => {
			if (targetId) await params.registry.untrackSessionBrowserTab(identity(targetId));
		},
		trackOpened: async (result, closeTab) => {
			const route = trackedRoute();
			const fallbackProfile = trackedProfile(route);
			const opened = readOpenedTab(result);
			const profile = opened.profile ?? fallbackProfile;
			try {
				await params.registry.trackSessionBrowserTab({
					sessionKey,
					targetId: opened.targetId,
					route,
					profile,
					...fallbackProfile && opened.profile && opened.profile !== fallbackProfile ? { profileAliases: [fallbackProfile] } : {},
					ownership: route.kind === "browser-control" && route.baseUrl ? void 0 : opened.ownership,
					aliases: opened.aliases,
					...params.authority ? { authority: params.authority } : {}
				});
			} catch (trackingError) {
				if (!opened.targetId) throw trackingError;
				try {
					await closeTab(opened.targetId, profile);
				} catch (closeError) {
					throw Object.assign(new Error("Failed to register browser tab cleanup and close the newly opened tab", { cause: closeError }), {
						name: "BrowserTabTrackingCompensationError",
						errors: [trackingError, closeError]
					});
				}
				throw trackingError;
			}
		}
	};
}
//#endregion
//#region extensions/browser/src/browser-tool.runtime.ts
function resolveRuntimeImageSanitization() {
	const maxDimensionPx = resolveOptionalIntegerOption(getRuntimeConfig().agents?.defaults?.imageMaxDimensionPx, { min: 1 });
	if (maxDimensionPx === void 0) return;
	return { maxDimensionPx };
}
//#endregion
//#region extensions/browser/src/browser/vision.ts
/**
* Browser screenshot description helpers built on the shared media image
* understanding contract. No browser-specific model registry lives here.
*/
/** Default prompt for turning browser screenshots into text-only page context. */
const DEFAULT_BROWSER_SCREENSHOT_DESCRIPTION_PROMPT = "Describe what is visible in this browser screenshot. Capture page layout, headings, primary content blocks, visible text, and notable interactive elements so a text-only assistant can reason about the page.";
function normalizeActiveModel(activeModel) {
	const provider = activeModel?.provider?.trim();
	if (!provider) return;
	const model = activeModel?.model?.trim();
	return model ? {
		provider,
		model
	} : { provider };
}
async function resolveImageUnderstandingFilePath(ctx) {
	const maxDimensionPx = ctx.imageSanitization?.maxDimensionPx;
	if (typeof maxDimensionPx !== "number" || !Number.isFinite(maxDimensionPx)) return ctx.filePath;
	const { normalizeBrowserScreenshot } = await import("./screenshot-DzZJ7ROu.mjs");
	const source = await readFile(ctx.filePath);
	const normalized = await normalizeBrowserScreenshot(source, { maxSide: Math.max(1, Math.floor(maxDimensionPx)) });
	if (normalized.buffer === source) return ctx.filePath;
	const { saveMediaBuffer } = await import("./plugin-sdk/media-runtime.js");
	return (await saveMediaBuffer(normalized.buffer, normalized.contentType ?? "image/jpeg", "browser")).path;
}
/** Produces a text description for a browser screenshot, or null when no text was produced. */
async function describeBrowserScreenshot(ctx) {
	const filePath = await resolveImageUnderstandingFilePath(ctx);
	const agentId = ctx.agentDir ? void 0 : (await import("./plugin-sdk/agent-scope-runtime.js")).resolveSessionAgentIdStrict({
		agentId: ctx.agentId,
		sessionKey: ctx.mediaScope?.sessionKey,
		config: ctx.cfg
	});
	const { describeImageFile } = await import("./plugin-sdk/media-understanding-runtime.js");
	const described = await describeImageFile({
		filePath,
		cfg: ctx.cfg,
		prompt: DEFAULT_BROWSER_SCREENSHOT_DESCRIPTION_PROMPT,
		...agentId ? { agentId } : {},
		agentDir: ctx.agentDir,
		workspaceDir: ctx.workspaceDir,
		activeModel: normalizeActiveModel(ctx.activeModel),
		scopeContext: ctx.mediaScope
	});
	const text = described.text?.trim();
	if (!text) return null;
	return {
		text,
		provider: described.provider,
		model: described.model,
		decision: described.decision
	};
}
/** Neutralizes model-generated MEDIA directives before feeding text back to tools. */
function neutralizeMediaDirectives(text) {
	if (!text || !/media:/i.test(text)) return text;
	return text.replace(/(^|\n)([^\S\n]*)(MEDIA:)/gi, "$1$2[neutralized] $3");
}
//#endregion
//#region extensions/browser/src/browser-tool.snapshot.ts
const BROWSER_EXTERNAL_JSON_TRUNCATION_MARKERS = {
	snapshot: "\n[truncated — retry with a smaller maxChars or limit]",
	console: "\n[truncated — retry with a stricter level or targetId]",
	requests: "\n[truncated — retry with a narrower filter or smaller limit]",
	errors: "\n[truncated — retry with a smaller limit]",
	tabs: "\n[truncated — retry with action=snapshot and a specific targetId]",
	act: "\n[truncated — inspect the affected targetId with action=snapshot]",
	download: "\n[truncated — retry with a specific targetId and download ref]"
};
function wrapBrowserExternalContent(value, includeWarning) {
	return wrapExternalContent(neutralizeMediaDirectives(value), {
		source: "browser",
		includeWarning
	});
}
function truncateBrowserToolText(value, marker, maxChars) {
	const bounded = truncateSanitizedExternalContent(value, maxChars);
	if (!bounded.truncated || marker.length > maxChars) return bounded;
	return {
		text: `${truncateSanitizedExternalContent(value, Math.max(0, maxChars - marker.length)).text}${marker}`,
		truncated: true
	};
}
/** Bound and wrap browser-originated text before it reaches the model. */
function wrapBrowserExternalText(params) {
	const wrap = (value) => wrapExternalContent(value, {
		source: "browser",
		includeWarning: params.includeWarning
	});
	const prefix = params.prefix ? `${params.prefix}\n` : "";
	const wrapperOverhead = prefix.length + wrap("").length;
	let maxInnerChars = Math.max(0, Math.min(params.maxChars ?? Infinity, DEFAULT_MAX_LIVE_TOOL_RESULT_CHARS - wrapperOverhead));
	const value = neutralizeMediaDirectives(params.value);
	let bounded = truncateBrowserToolText(value, params.marker, maxInnerChars);
	let wrappedText = prefix + wrap(bounded.text);
	if (wrappedText.length > 16e3) {
		maxInnerChars = Math.max(0, maxInnerChars - (wrappedText.length - DEFAULT_MAX_LIVE_TOOL_RESULT_CHARS));
		bounded = truncateBrowserToolText(value, params.marker, maxInnerChars);
		wrappedText = prefix + wrap(bounded.text);
	}
	return {
		text: wrappedText,
		boundedText: bounded.text,
		truncated: bounded.truncated
	};
}
/** Wrap page-controlled JSON payloads as untrusted browser content. */
function wrapBrowserExternalJson(params) {
	const wrapped = wrapBrowserExternalText({
		value: JSON.stringify(params.payload, (_key, value) => typeof value === "string" ? neutralizeMediaDirectives(value) : value, 2) ?? "null",
		marker: BROWSER_EXTERNAL_JSON_TRUNCATION_MARKERS[params.kind],
		includeWarning: params.includeWarning ?? true
	});
	return {
		wrappedText: wrapped.text,
		truncated: wrapped.truncated,
		safeDetails: {
			ok: true,
			externalContent: {
				untrusted: true,
				source: "browser",
				kind: params.kind,
				wrapped: true
			}
		}
	};
}
/** Keep debug log counts aligned with complete records inside the output budget. */
function formatBrowserDebugLogResult(kind, result, entries, limit) {
	const total = entries.length;
	let records = entries.slice(-limit);
	const details = () => ({
		ok: result.ok,
		targetId: result.targetId,
		url: result.url,
		total,
		returned: records.length,
		truncated: records.length < total
	});
	const wrap = () => wrapBrowserExternalJson({
		kind,
		payload: {
			...details(),
			[kind]: records
		},
		includeWarning: false
	});
	let wrapped = wrap();
	if (wrapped.truncated && records.length > 0) {
		const initialRecords = records;
		let lower = 1;
		let upper = initialRecords.length;
		while (lower < upper) {
			const middle = Math.floor((lower + upper) / 2);
			records = initialRecords.slice(middle);
			wrapped = wrap();
			if (wrapped.truncated) lower = middle + 1;
			else upper = middle;
		}
		if (records.length !== initialRecords.length - lower) {
			records = initialRecords.slice(lower);
			wrapped = wrap();
		}
	}
	return textResult(wrapped.wrappedText, {
		...wrapped.safeDetails,
		...details(),
		truncated: records.length < total || wrapped.truncated
	});
}
function isAriaRefsUnsupportedError(err) {
	const msg = String(err).toLowerCase();
	return msg.includes("refs=aria") && msg.includes("not support");
}
async function executeSnapshotAction(params) {
	const { input, target, profile } = params;
	const snapshotDefaults = getRuntimeConfig().browser?.snapshotDefaults;
	const format = input.snapshotFormat === "ai" ? "ai" : input.snapshotFormat === "aria" ? "aria" : void 0;
	const formatExplicit = format !== void 0;
	const mode = input.mode === "efficient" || !formatExplicit && snapshotDefaults?.mode === "efficient" ? "efficient" : void 0;
	const labels = typeof input.labels === "boolean" ? input.labels : void 0;
	const urls = typeof input.urls === "boolean" ? input.urls : void 0;
	const refs = input.refs === "aria" || input.refs === "role" ? input.refs : void 0;
	const hasMaxChars = Object.hasOwn(input, "maxChars");
	const targetId = normalizeOptionalString(input.targetId);
	const limit = readPositiveIntegerParam(input, "limit", { message: "limit must be a positive integer." });
	const maxCharsRaw = readNonNegativeIntegerParam(input, "maxChars", { message: "maxChars must be a non-negative integer." });
	const maxChars = maxCharsRaw !== void 0 && maxCharsRaw > 0 ? maxCharsRaw : void 0;
	const interactive = typeof input.interactive === "boolean" ? input.interactive : void 0;
	const compact = typeof input.compact === "boolean" ? input.compact : void 0;
	const depth = readNonNegativeIntegerParam(input, "depth", { message: "depth must be a non-negative integer." });
	const selector = normalizeOptionalString(input.selector);
	const frame = normalizeOptionalString(input.frame);
	const resolvedMaxChars = hasMaxChars ? maxChars : format === "ai" && mode !== "efficient" ? DEFAULT_AI_SNAPSHOT_MAX_CHARS : void 0;
	const snapshotTimeoutMs = readPositiveIntegerParam(input, "timeoutMs", { message: "timeoutMs must be a positive integer." }) ?? 2e4;
	const snapshotQuery = {
		...format ? { format } : {},
		targetId,
		limit,
		...typeof resolvedMaxChars === "number" ? { maxChars: resolvedMaxChars } : {},
		refs,
		interactive,
		compact,
		depth,
		selector,
		frame,
		labels,
		urls,
		mode,
		timeoutMs: snapshotTimeoutMs
	};
	let refsFallback;
	const readSnapshot = async (query) => await browserSnapshot(target, {
		...query,
		profile,
		signal: params.signal
	});
	let snapshot;
	try {
		snapshot = await readSnapshot(snapshotQuery);
	} catch (err) {
		if (refs !== "aria" || !isAriaRefsUnsupportedError(err)) throw err;
		refsFallback = "role";
		snapshot = await readSnapshot({
			...snapshotQuery,
			refs: "role"
		});
	}
	await params.onTabActivity?.(readStringValue(snapshot.targetId) ?? targetId);
	const identity = {
		format: snapshot.format,
		targetId: snapshot.targetId,
		url: snapshot.url
	};
	const dialogState = {
		...snapshot.blockedByDialog ? { blockedByDialog: true } : {},
		...snapshot.browserState !== void 0 ? { browserState: snapshot.browserState } : {}
	};
	const aiMetadata = snapshot.format === "ai" ? {
		labels: snapshot.labels,
		labelsCount: snapshot.labelsCount,
		labelsSkipped: snapshot.labelsSkipped,
		annotations: snapshot.annotations,
		imagePath: snapshot.imagePath,
		imageType: snapshot.imageType,
		refsFallback
	} : {};
	const externalContent = {
		untrusted: true,
		source: "browser",
		kind: "snapshot",
		format: snapshot.format,
		wrapped: true
	};
	const finishSnapshot = async (text, details) => {
		if (labels && snapshot.format === "ai" && snapshot.imagePath) return await imageResultFromFile({
			label: "browser:snapshot",
			path: snapshot.imagePath,
			extraText: text,
			details: {
				...details,
				media: { outbound: false }
			},
			imageSanitization: resolveRuntimeImageSanitization()
		});
		return textResult(text, details);
	};
	const query = normalizeOptionalString(input.query);
	if (query && !snapshot.blockedByDialog) {
		const tokens = query.toLowerCase().split(/\s+/);
		const matches = (snapshot.format === "ai" ? snapshot.snapshot : snapshot.nodes.map((node) => `- ${node.role} ${JSON.stringify(node.name)} [ref=${node.ref}]${node.value ? ` value=${JSON.stringify(node.value)}` : ""}${node.description ? ` description=${JSON.stringify(node.description)}` : ""}`).join("\n")).split("\n").filter((line) => {
			const lower = line.toLowerCase();
			return tokens.every((token) => lower.includes(token));
		});
		const matchedText = matches.join("\n");
		const matchCount = matches.length;
		const summary = matchCount ? `${matchCount} matching line(s) in the returned snapshot${snapshot.truncated ? " (source truncated)" : ""}.` : "No matching lines in the returned snapshot. Refine the query or take a broader snapshot.";
		const wrapped = wrapBrowserExternalText({
			value: matchedText,
			marker: BROWSER_EXTERNAL_JSON_TRUNCATION_MARKERS.snapshot,
			maxChars: maxChars ?? 4e4,
			includeWarning: true,
			prefix: summary
		});
		const snapshotRefs = snapshot.format === "ai" ? snapshot.refs ?? {} : Object.fromEntries(snapshot.nodes.map((node) => [node.ref, {
			role: node.role,
			name: node.name
		}]));
		const filtered = finalizeRoleSnapshot({
			snapshot: wrapped.boundedText,
			refs: snapshotRefs
		});
		const newElements = filtered.snapshot.split("\n").filter((line) => line.endsWith(" [new]") && findRoleSnapshotLineRef(line)).length;
		return await finishSnapshot(wrapped.text, {
			ok: snapshot.ok,
			...identity,
			matchCount,
			stats: filtered.stats,
			refs: filtered.stats.refs,
			...snapshot.format === "ai" && snapshot.newElements !== void 0 ? { newElements } : {},
			truncated: snapshot.truncated || wrapped.truncated || void 0,
			...dialogState,
			...aiMetadata,
			externalContent
		});
	}
	if (snapshot.format === "ai" && !snapshot.blockedByDialog) {
		const boundedSnapshot = wrapBrowserExternalText({
			value: snapshot.snapshot ?? "",
			marker: BROWSER_EXTERNAL_JSON_TRUNCATION_MARKERS.snapshot,
			includeWarning: true
		});
		return await finishSnapshot(boundedSnapshot.text, {
			ok: true,
			...identity,
			truncated: snapshot.truncated || boundedSnapshot.truncated ? true : void 0,
			newElements: snapshot.newElements,
			stats: snapshot.stats,
			refs: snapshot.refs ? Object.keys(snapshot.refs).length : void 0,
			...aiMetadata,
			...dialogState,
			externalContent
		});
	}
	const wrapped = wrapBrowserExternalJson({
		kind: "snapshot",
		payload: snapshot.format === "ai" ? {
			...identity,
			...dialogState
		} : snapshot
	});
	return textResult(wrapped.wrappedText, {
		...wrapped.safeDetails,
		...identity,
		...snapshot.format === "ai" ? dialogState : {
			nodeCount: snapshot.nodes.length,
			...dialogState,
			externalContent
		}
	});
}
/**
* Attach fresh page state to the result of an action that changed the page
* document (navigate, act that navigated). The model can act on the new page
* without a follow-up snapshot call. The inline state uses the efficient
* interactive tier so the unsolicited payload stays bounded on every profile
* (mode=efficient forces the capped ai format even where the profile default
* would be an uncapped aria tree); a full snapshot stays one explicit call away.
*/
async function appendNavigatedPageState(params) {
	let snapshot;
	try {
		snapshot = await executeSnapshotAction({
			input: {
				targetId: params.targetId,
				mode: "efficient"
			},
			target: params.target,
			profile: params.profile,
			signal: params.signal
		});
	} catch (err) {
		params.signal?.throwIfAborted();
		if (err instanceof Error && err.name === "AbortError") throw err;
		const reason = wrapBrowserExternalContent(formatErrorMessage(err), false);
		return {
			...params.result,
			content: [...params.result.content, {
				type: "text",
				text: `[page snapshot unavailable: ${reason}. Use action=snapshot to read the page.]`
			}]
		};
	}
	return {
		content: [...params.result.content, ...snapshot.content],
		details: {
			...asRecord(params.result.details),
			pageState: snapshot.details
		}
	};
}
//#endregion
//#region extensions/browser/src/browser/client-actions.ts
function resolveBrowserOperationRequestTimeoutMs(timeoutMs) {
	const operationTimeoutMs = clampPositiveTimerTimeoutMs(timeoutMs) ?? 12e4;
	return addTimerTimeoutGraceMs(operationTimeoutMs, 5e3) ?? 1;
}
function projectBrowserOptions(opts, fields) {
	return Object.fromEntries(fields.map((key) => [key, opts[key]]));
}
async function browserNavigate(baseUrl, opts) {
	const timeoutMs = resolveBrowserNavigationTimeoutMs(opts.timeoutMs);
	return await postBrowserJson(baseUrl, "/navigate", {
		url: opts.url,
		targetId: opts.targetId,
		timeoutMs
	}, resolveBrowserOperationRequestTimeoutMs(timeoutMs), opts);
}
function createBrowserOperation(path, fields, timeoutScope) {
	return async (baseUrl, opts) => await postBrowserJson(baseUrl, path, projectBrowserOptions(opts, fields), timeoutScope === "local" ? browserClientTimeout(baseUrl, void 0, resolveBrowserOperationRequestTimeoutMs(opts.timeoutMs)) : resolveBrowserOperationRequestTimeoutMs(opts.timeoutMs), opts);
}
const browserArmDialog = createBrowserOperation("/hooks/dialog", [
	"accept",
	"promptText",
	"dialogId",
	"targetId",
	"timeoutMs"
], "local");
const browserArmFileChooser = createBrowserOperation("/hooks/file-chooser", [
	"paths",
	"ref",
	"inputRef",
	"element",
	"targetId",
	"timeoutMs"
], "local");
const browserWaitForDownload = createBrowserOperation("/wait/download", [
	"targetId",
	"path",
	"timeoutMs"
], "all");
const browserDownload = createBrowserOperation("/download", [
	"targetId",
	"ref",
	"path",
	"timeoutMs"
], "all");
async function browserAct(baseUrl, req, opts) {
	return await postBrowserJson(baseUrl, "/act", req, resolveTimerTimeoutMs(opts?.timeoutMs, resolveBrowserActRequestTimeoutMs(req)), opts);
}
async function browserScreenshotAction(baseUrl, opts) {
	const effectiveTimeoutMs = clampPositiveTimerTimeoutMs(opts.timeoutMs) ?? 2e4;
	return await postBrowserJson(baseUrl, "/screenshot", {
		...projectBrowserOptions(opts, [
			"targetId",
			"fullPage",
			"ref",
			"element",
			"type",
			"labels"
		]),
		timeoutMs: effectiveTimeoutMs
	}, effectiveTimeoutMs, opts);
}
function buildQuery(params) {
	return Object.fromEntries(Object.entries(params).filter(([, value]) => typeof value === "boolean" || typeof value === "string" && value.length > 0));
}
function readBrowserPageJson(baseUrl, path, opts, query) {
	return requestBrowserJson(baseUrl, path, {
		query,
		profile: opts.profile,
		timeoutMs: browserClientTimeout(baseUrl, void 0, 2e4),
		signal: opts.signal
	});
}
async function browserConsoleMessages(baseUrl, opts = {}) {
	return await readBrowserPageJson(baseUrl, "/console", opts, buildQuery({
		level: opts.level,
		targetId: opts.targetId
	}));
}
async function browserRequests(baseUrl, opts = {}) {
	return await readBrowserPageJson(baseUrl, "/requests", opts, buildQuery({
		filter: opts.filter,
		clear: opts.clear,
		targetId: opts.targetId
	}));
}
async function browserErrors(baseUrl, opts = {}) {
	return await readBrowserPageJson(baseUrl, "/errors", opts, buildQuery({
		clear: opts.clear,
		targetId: opts.targetId
	}));
}
/** Read bounded visible text without executing page-supplied code. */
async function browserPageText(baseUrl, opts) {
	return await readBrowserPageJson(baseUrl, "/text", opts, {
		...buildQuery({
			targetId: opts.targetId,
			selector: opts.selector
		}),
		maxChars: opts.maxChars
	});
}
async function browserEmulateSetting(baseUrl, opts) {
	return await postBrowserJson(baseUrl, `/set/${opts.setting}`, opts.body, browserClientTimeout(baseUrl, void 0, 2e4), opts);
}
async function browserPdfSave(baseUrl, opts = {}) {
	return await postBrowserJson(baseUrl, "/pdf", { targetId: opts.targetId }, browserClientTimeout(baseUrl, void 0, 2e4), opts);
}
//#endregion
//#region extensions/browser/src/browser-tool.actions.ts
function normalizePositiveTimeoutMs(value) {
	return readPositiveIntegerParam({ value }, "value", { message: "timeoutMs must be a positive integer." });
}
function normalizeNonNegativeDurationMs(value) {
	return readNonNegativeIntegerParam({ value }, "value", { message: "timeMs must be a non-negative integer." });
}
function withLocalActTimeout(request, usesChromeMcp) {
	if (normalizePositiveTimeoutMs("timeoutMs" in request ? request.timeoutMs : void 0) !== void 0 || usesChromeMcp && !EXISTING_SESSION_TIMEOUT_OVERRIDE_KINDS.has(request.kind)) return request;
	switch (request.kind) {
		case "click":
		case "type":
		case "hover":
		case "scrollIntoView":
		case "drag":
		case "select":
		case "fill":
		case "evaluate":
		case "wait": return {
			...request,
			timeoutMs: DEFAULT_BROWSER_ACTION_TIMEOUT_MS
		};
		default: return request;
	}
}
function formatAgentTab(tab) {
	const source = asNullableObjectRecord(tab);
	if (!source) return { value: tab };
	const targetId = readStringValue(source.targetId);
	const tabId = readStringValue(source.tabId);
	const webExtensionTabId = typeof source.webExtensionTabId === "number" && Number.isSafeInteger(source.webExtensionTabId) && source.webExtensionTabId >= 0 ? source.webExtensionTabId : void 0;
	const label = readStringValue(source.label);
	const suggestedTargetId = readStringValue(source.suggestedTargetId) ?? label ?? tabId ?? targetId;
	return {
		...suggestedTargetId ? { suggestedTargetId } : {},
		...tabId ? { tabId } : {},
		...webExtensionTabId !== void 0 ? { webExtensionTabId } : {},
		...label ? { label } : {},
		title: source.title,
		url: source.url,
		...source.urlUnavailableReason === "navigation_blocked" || source.urlUnavailableReason === "navigation_check_failed" ? { urlUnavailableReason: source.urlUnavailableReason } : {},
		type: source.type,
		...targetId ? { targetId } : {},
		...source.wsUrl ? { wsUrl: source.wsUrl } : {}
	};
}
function formatTabsToolResult(result) {
	const formattedTabs = result.tabs.map(formatAgentTab);
	const wrapped = wrapBrowserExternalJson({
		kind: "tabs",
		payload: {
			running: result.running,
			tabs: formattedTabs
		},
		includeWarning: false
	});
	return textResult(wrapped.wrappedText, {
		...wrapped.safeDetails,
		running: result.running,
		tabCount: formattedTabs.length,
		tabs: formattedTabs
	});
}
/** Protect page-controlled model text while preserving the shipped structured result contract. */
function formatBrowserExternalToolResult(params) {
	const wrapped = wrapBrowserExternalJson({
		kind: params.kind,
		payload: params.payload,
		includeWarning: false
	});
	return textResult(wrapped.wrappedText, params.payload);
}
function isChromeStaleTargetError(usesChromeMcp, err) {
	const status = err && typeof err === "object" && "status" in err ? err.status : null;
	const msg = String(err);
	const isTabNotFound = (status === 404 || msg.includes("404:")) && msg.includes("tab not found");
	return usesChromeMcp && isTabNotFound;
}
function canRetryChromeActAfterSoleTargetRefresh(request) {
	if (request.kind !== "wait" || normalizeNonNegativeDurationMs(request.timeMs) === void 0) return false;
	return [
		request.fn,
		request.text,
		request.textGone,
		request.selector,
		request.url,
		request.loadState
	].every((value) => !normalizeOptionalString(value));
}
async function executeTabsAction(params) {
	const { target, profile, timeoutMs } = params;
	const result = await browserTabs(target, {
		profile,
		timeoutMs,
		signal: params.signal
	});
	const tabs = result.tabs.filter((tab) => !params.targetId || readStringValue(tab.targetId) === params.targetId);
	return formatTabsToolResult({
		running: result.running,
		tabs
	});
}
/** Validate the /act wire payload's abort summary once for note and page-state decisions. */
function readBrowserBatchAbort(result) {
	const aborted = asNullableObjectRecord(asNullableObjectRecord(result)?.aborted);
	if (!aborted) return null;
	const { reason, afterAction, url, skipped } = aborted;
	if (reason !== "navigation" && reason !== "closed" || typeof afterAction !== "number" || typeof url !== "string" || typeof skipped !== "number") return null;
	return {
		reason,
		afterAction,
		url,
		skipped
	};
}
function actObservedNavigation(result, aborted) {
	if (aborted?.reason === "navigation") return true;
	const results = result?.results;
	return Array.isArray(results) && results.some((entry) => entry?.navigated === true);
}
async function executeConsoleAction(params) {
	const { input, target, profile } = params;
	const result = await browserConsoleMessages(target, {
		level: normalizeOptionalString(input.level),
		targetId: normalizeOptionalString(input.targetId),
		profile,
		signal: params.signal
	});
	const wrapped = wrapBrowserExternalJson({
		kind: "console",
		payload: result,
		includeWarning: false
	});
	return textResult(wrapped.wrappedText, {
		...wrapped.safeDetails,
		targetId: readStringValue(result.targetId),
		url: readStringValue(result.url),
		messageCount: Array.isArray(result.messages) ? result.messages.length : void 0
	});
}
/** Read browser debug logs, keeping counts aligned with the bounded payload. */
async function executeDebugLogAction(kind, params) {
	const { input, target, profile, signal } = params;
	const limit = readPositiveIntegerParam(input, "limit", { message: "limit must be a positive integer." }) ?? 50;
	const options = {
		targetId: normalizeOptionalString(input.targetId),
		clear: typeof input.clear === "boolean" ? input.clear : void 0,
		profile,
		signal
	};
	if (kind === "requests") {
		const result = await browserRequests(target, {
			...options,
			filter: normalizeOptionalString(input.filter)
		});
		return formatBrowserDebugLogResult(kind, result, result.requests, limit);
	}
	const result = await browserErrors(target, options);
	return formatBrowserDebugLogResult("errors", result, result.errors, limit);
}
/** Extract visible page prose with the same trust boundary as snapshots. */
async function executeTextAction(params) {
	const { input, target, profile, signal } = params;
	const targetId = normalizeOptionalString(input.targetId);
	const selector = normalizeOptionalString(input.selector);
	const maxChars = Math.min(readPositiveIntegerParam(input, "maxChars", { message: "maxChars must be a positive integer." }) ?? 4e4, DEFAULT_AI_SNAPSHOT_MAX_CHARS);
	const result = await browserPageText(target, {
		targetId,
		selector,
		maxChars,
		profile,
		signal
	});
	const wrapped = wrapBrowserExternalText({
		value: result.text,
		marker: "\n[truncated — retry with a narrower selector]",
		includeWarning: true,
		maxChars,
		prefix: result.truncated ? "Page text was truncated. Retry with a narrower selector." : void 0
	});
	return textResult(wrapped.text, {
		ok: result.ok,
		targetId: result.targetId,
		url: result.url,
		truncated: result.truncated || wrapped.truncated,
		externalContent: {
			untrusted: true,
			source: "browser",
			kind: "text",
			wrapped: true
		}
	});
}
/** Apply settings in order and pin later changes to the first resolved tab. */
async function executeEmulateAction(params) {
	const { input, target, profile, signal } = params;
	const requested = [
		[
			"device",
			"device",
			"name"
		],
		[
			"colorScheme",
			"media",
			"colorScheme"
		],
		[
			"timezoneId",
			"timezone",
			"timezoneId"
		],
		[
			"locale",
			"locale",
			"locale"
		]
	].flatMap(([field, setting, key]) => {
		const value = normalizeOptionalString(input[field]);
		return value ? [{
			field,
			setting,
			key,
			value
		}] : [];
	});
	if (requested.length === 0) throw new Error("emulate requires at least one of device, colorScheme, timezoneId, or locale.");
	const colorScheme = requested.find(({ field }) => field === "colorScheme")?.value;
	if (colorScheme && ![
		"dark",
		"light",
		"no-preference",
		"none"
	].includes(colorScheme)) throw new Error("colorScheme must be dark|light|no-preference|none.");
	let targetId = normalizeOptionalString(input.targetId);
	for (const { setting, key, value } of requested) targetId = (await browserEmulateSetting(target, {
		setting,
		body: {
			targetId,
			[key]: value
		},
		profile,
		signal
	})).targetId ?? targetId;
	return jsonResult({
		ok: true,
		targetId,
		applied: requested.map(({ field }) => field)
	});
}
async function executeDownloadAction(params) {
	const { action, input, target, profile } = params;
	const targetId = normalizeOptionalString(input.targetId);
	const options = {
		targetId,
		timeoutMs: normalizePositiveTimeoutMs(input.timeoutMs),
		profile,
		signal: params.signal
	};
	const result = action === "download" ? await browserDownload(target, {
		...options,
		ref: readToolStringParam(input, "ref", { required: true }),
		path: readToolStringParam(input, "path", { required: true })
	}) : await browserWaitForDownload(target, {
		...options,
		path: readToolStringParam(input, "path")
	});
	await params.onTabActivity?.(readStringValue(result.targetId) ?? targetId);
	return formatBrowserExternalToolResult({
		kind: "download",
		payload: result
	});
}
/** Execute browser actions with route-owned timeout semantics and stale-tab recovery. */
async function executeActAction(params) {
	const { request, baseUrl, profile, proxyRequest } = params;
	if ("timeoutMs" in request && request.timeoutMs !== void 0) normalizePositiveTimeoutMs(request.timeoutMs);
	const effectiveRequest = proxyRequest ? request : withLocalActTimeout(request, params.usesChromeMcp);
	const finishActResult = async (result, resolvedTargetId) => {
		const aborted = readBrowserBatchAbort(result);
		await (effectiveRequest.kind === "close" || aborted?.reason === "closed" ? params.onTabClose : params.onTabActivity)?.(resolvedTargetId);
		const formatted = formatActToolResult(result, aborted);
		if (!actObservedNavigation(result, aborted)) return formatted;
		return await appendNavigatedPageState({
			result: formatted,
			targetId: resolvedTargetId,
			target: proxyRequest ?? baseUrl,
			profile,
			signal: params.signal
		});
	};
	const dispatchAct = async (actionRequest) => {
		const result = await browserAct(proxyRequest ?? baseUrl, actionRequest, {
			profile,
			signal: params.signal
		});
		return {
			result,
			targetId: readStringValue(result.targetId) ?? readStringValue(actionRequest.targetId)
		};
	};
	let dispatched;
	try {
		dispatched = await dispatchAct(effectiveRequest);
	} catch (err) {
		const proxyRoute = proxyRequest?.route();
		const usesChromeMcp = proxyRequest ? proxyRoute?.status === "resolved" && proxyRoute.driver === "existing-session" : params.usesChromeMcp;
		const recoveryProfile = proxyRoute?.status === "resolved" ? proxyRoute.profile : profile ?? "default";
		if (isChromeStaleTargetError(usesChromeMcp, err)) {
			let tabRefreshError;
			const availability = await browserTabs(proxyRequest ?? baseUrl, {
				profile,
				signal: params.signal
			}).catch((refreshError) => {
				params.signal?.throwIfAborted();
				tabRefreshError = refreshError;
				return {
					running: false,
					tabs: []
				};
			});
			const tabs = availability.tabs;
			const freshTargetId = tabs.length === 1 ? readStringValue(tabs[0]?.targetId) : void 0;
			const retryRequest = freshTargetId && normalizeOptionalString(effectiveRequest.targetId) ? {
				...effectiveRequest,
				targetId: freshTargetId
			} : null;
			if (retryRequest && canRetryChromeActAfterSoleTargetRefresh(effectiveRequest)) {
				const retried = await dispatchAct(retryRequest);
				return await finishActResult(retried.result, retried.targetId);
			}
			if (tabRefreshError) throw new Error(`Chrome tab not found for profile="${recoveryProfile}", and refreshing tabs failed: ${formatErrorMessage(tabRefreshError)}. Run action=tabs profile="${recoveryProfile}" and retry with a returned targetId.`, { cause: err });
			if (!availability.running) throw new Error(`Browser tabs are unavailable for profile="${recoveryProfile}". Reconnect or start that browser profile, then run action=tabs and retry.`, { cause: err });
			if (!tabs.length) throw new Error(`No browser tabs found for profile="${recoveryProfile}". Make sure the configured Chromium-based browser (v144+) is running and has open tabs, then retry.`, { cause: err });
			throw new Error(`Chrome tab not found (stale targetId?). Run action=tabs profile="${recoveryProfile}" and use one of the returned targetIds.`, { cause: err });
		}
		throw err;
	}
	return await finishActResult(dispatched.result, dispatched.targetId);
}
function formatActToolResult(result, aborted) {
	const formatted = formatBrowserExternalToolResult({
		kind: "act",
		payload: result
	});
	if (!aborted) return formatted;
	const note = aborted.reason === "navigation" ? `Batch aborted after action ${aborted.afterAction} because the page navigated; ${aborted.skipped} remaining action(s) skipped. Earlier refs are stale.` : `Batch aborted after action ${aborted.afterAction} because the page or browser context closed; ${aborted.skipped} remaining action(s) skipped. Take a new snapshot before continuing.`;
	return {
		...formatted,
		content: [...formatted.content, {
			type: "text",
			text: note
		}]
	};
}
//#endregion
//#region extensions/browser/src/browser/screenshot-sharing.ts
/** Stages a bounded screenshot copy in the sandbox-authorized outbound store. */
async function stageBrowserScreenshotForSharing(filePath, maxDimensionPx) {
	const source = await readFile(filePath);
	const normalized = await normalizeBrowserScreenshot(source, {
		maxSide: maxDimensionPx ?? 2e3,
		maxBytes: DEFAULT_BROWSER_SCREENSHOT_MAX_BYTES
	});
	return (await saveMediaBuffer(normalized.buffer, normalized.contentType, "outbound", DEFAULT_BROWSER_SCREENSHOT_MAX_BYTES, path.basename(filePath))).path;
}
//#endregion
//#region extensions/browser/src/browser-tool.screenshot.ts
const SCREENSHOT_SHARE_UNAVAILABLE = "[Screenshot sharing is unavailable because an outbound copy could not be prepared.]";
async function executeScreenshotAction({ input: params, target, profile, requestedTimeoutMs, signal, onTabActivity, opts }) {
	const targetId = readToolStringParam(params, "targetId");
	const type = params.type === "jpeg" ? "jpeg" : "png";
	const result = await browserScreenshotAction(target, {
		targetId,
		fullPage: Boolean(params.fullPage),
		ref: readToolStringParam(params, "ref"),
		element: readToolStringParam(params, "element"),
		type,
		labels: typeof params.labels === "boolean" ? params.labels : void 0,
		timeoutMs: requestedTimeoutMs ?? 2e4,
		profile,
		signal
	});
	await onTabActivity(readStringValue(result.targetId) ?? targetId);
	if (opts?.screenshotResultMode === "path") {
		const artifactPath = opts.persistScreenshot ? await opts.persistScreenshot({
			sourcePath: result.path,
			type,
			targetId: readStringValue(result.targetId) ?? targetId
		}) : result.path;
		if (artifactPath.length > 4096) throw new Error("Browser screenshot artifact path exceeds 4096 characters");
		const resultTargetId = readStringValue(result.targetId) ?? targetId;
		const resultUrl = readStringValue(result.url);
		return jsonResult({
			ok: result.ok,
			path: artifactPath,
			...resultTargetId ? { targetId: truncateUtf16Safe(resultTargetId, 256) } : {},
			...resultUrl ? { url: truncateUtf16Safe(resultUrl, 2048) } : {},
			...Array.isArray(result.annotations) ? { annotationCount: result.annotations.length } : {},
			media: { outbound: false }
		});
	}
	const screenshotPath = result.path;
	const screenshotCfg = getRuntimeConfig();
	const imageSanitization = resolveRuntimeImageSanitization();
	let shareHint = SCREENSHOT_SHARE_UNAVAILABLE;
	try {
		const sharePath = await stageBrowserScreenshotForSharing(screenshotPath, imageSanitization?.maxDimensionPx);
		shareHint = `[Screenshot saved to ${JSON.stringify(sharePath)}. A sanitized outbound copy is ready at this path for explicit sharing.]`;
	} catch {}
	const screenshotDetails = {
		...result,
		media: { outbound: false }
	};
	let extraText = shareHint;
	try {
		const described = await describeBrowserScreenshot({
			cfg: screenshotCfg,
			filePath: screenshotPath,
			agentDir: opts?.agentDir,
			agentId: opts?.agentId,
			workspaceDir: opts?.workspaceDir,
			activeModel: opts?.activeModel,
			mediaScope: opts?.mediaScope,
			imageSanitization
		});
		if (described) {
			const text = `[analyzed by ${described.provider && described.model ? `${described.provider}/${described.model}` : "media image understanding"}]\n${wrapBrowserExternalContent(described.text.trim(), true)}\n${shareHint}`;
			return textResult(text, {
				...result,
				vision: {
					provider: described.provider,
					model: described.model,
					decision: described.decision
				}
			});
		}
	} catch (err) {
		extraText = `[browser screenshot vision failed: ${wrapBrowserExternalContent(err instanceof Error ? err.message : String(err), false)}]\n${shareHint}`;
	}
	return await imageResultFromFile({
		label: "browser:screenshot",
		path: screenshotPath,
		extraText,
		details: screenshotDetails,
		imageSanitization
	});
}
//#endregion
//#region extensions/browser/src/browser-tool-dispatch.ts
function readOptionalTargetAndTimeout(params) {
	return {
		targetId: normalizeOptionalString(params.targetId),
		timeoutMs: readPositiveIntegerParam(params, "timeoutMs", { message: "timeoutMs must be a positive integer." })
	};
}
function readTargetUrlParam(params) {
	const targetUrl = readToolStringParam(params, "targetUrl") ?? readToolStringParam(params, "url", {
		required: true,
		label: "targetUrl"
	});
	parseBrowserNavigationUrl(targetUrl);
	return targetUrl;
}
async function executeBrowserTabAction(context) {
	const { action, params, baseUrl, profile, proxyRequest, nodeRoute, sessionTabs, capabilities, isUserBrowserProfile, toolTimeoutMs, requestedTimeoutMs, signal, opts } = context;
	const touchTab = async (targetId) => {
		await sessionTabs.touch(targetId);
		await context.onTabActivity(targetId);
	};
	const trackedTabResult = async (result, targetId) => {
		await touchTab(readStringValue(asNullableRecord(result)?.targetId) ?? targetId);
		return jsonResult(result);
	};
	const target = proxyRequest ?? baseUrl;
	const actionOptions = {
		input: params,
		target,
		profile,
		signal
	};
	const clientOptions = {
		profile,
		timeoutMs: toolTimeoutMs,
		signal
	};
	switch (action) {
		case "tabs": return await executeTabsAction({
			target,
			profile,
			timeoutMs: toolTimeoutMs,
			targetId: context.boundTargetId,
			signal
		});
		case "open": {
			const targetUrl = readTargetUrlParam(params);
			const label = normalizeOptionalString(params.label);
			const opened = await browserOpenTab(target, targetUrl, {
				...clientOptions,
				label
			});
			const closeOpenedTab = async (targetId, openedProfile) => {
				if (nodeRoute && !proxyRequest?.isHostFallbackActive()) {
					await nodeRoute.closeTarget({
						targetId,
						profile: openedProfile
					});
					return;
				}
				await browserCloseTab(baseUrl, targetId, {
					profile: openedProfile,
					timeoutMs: toolTimeoutMs
				});
			};
			await sessionTabs.trackOpened(opened, closeOpenedTab);
			await context.onTabActivity(readStringValue(asNullableRecord(opened)?.targetId), readStringValue(asNullableRecord(opened)?.resolvedProfile));
			return formatBrowserExternalToolResult({
				kind: "tabs",
				payload: stripBrowserOpenInternalMetadata(opened)
			});
		}
		case "focus": {
			const targetId = readToolStringParam(params, "targetId", { required: true });
			return await trackedTabResult(await browserFocusTab(target, targetId, clientOptions), targetId);
		}
		case "close": {
			const targetId = readToolStringParam(params, "targetId");
			const result = targetId ? await browserCloseTab(target, targetId, clientOptions) : await browserAct(target, { kind: "close" }, clientOptions);
			await sessionTabs.untrack(readStringValue(asNullableRecord(result)?.targetId) ?? targetId);
			return jsonResult(result);
		}
		case "snapshot": return await executeSnapshotAction({
			...actionOptions,
			onTabActivity: touchTab
		});
		case "screenshot": return await executeScreenshotAction({
			...actionOptions,
			requestedTimeoutMs,
			onTabActivity: touchTab,
			opts
		});
		case "navigate": {
			const targetUrl = readTargetUrlParam(params);
			const targetId = readToolStringParam(params, "targetId");
			const result = await browserNavigate(target, {
				url: targetUrl,
				targetId,
				timeoutMs: requestedTimeoutMs,
				profile,
				signal
			});
			const navigatedTargetId = readStringValue(asNullableRecord(result)?.targetId) ?? targetId;
			await touchTab(navigatedTargetId);
			const formatted = formatBrowserExternalToolResult({
				kind: asNullableRecord(result)?.download ? "download" : "act",
				payload: result
			});
			if (asNullableRecord(result)?.download) return formatted;
			return await appendNavigatedPageState({
				result: formatted,
				targetId: navigatedTargetId,
				target,
				profile,
				signal
			});
		}
		case "console": {
			const result = await executeConsoleAction(actionOptions);
			const targetId = readToolStringParam(params, "targetId");
			await touchTab(readStringValue(asNullableRecord(result.details)?.targetId) ?? targetId);
			return result;
		}
		case "requests":
		case "errors":
		case "text":
		case "emulate": {
			const result = action === "requests" || action === "errors" ? await executeDebugLogAction(action, actionOptions) : await (action === "text" ? executeTextAction : executeEmulateAction)(actionOptions);
			await touchTab(readStringValue(asNullableRecord(result.details)?.targetId) ?? readStringValue(params.targetId));
			return result;
		}
		case "pdf": {
			const targetId = normalizeOptionalString(params.targetId);
			const result = await browserPdfSave(target, {
				targetId,
				profile,
				signal
			});
			await touchTab(readStringValue(result.targetId) ?? targetId);
			return textResult(`FILE:${result.path}`, result);
		}
		case "download":
		case "waitfordownload": return await executeDownloadAction({
			...actionOptions,
			action,
			onTabActivity: touchTab
		});
		case "upload": {
			const paths = Array.isArray(params.paths) ? params.paths.map((p) => String(p)) : [];
			if (paths.length === 0) throw new Error("paths required");
			const resolvedResult = await resolveExistingUploadPaths({ requestedPaths: paths });
			if (!resolvedResult.ok) throw new Error(resolvedResult.error);
			const ref = readToolStringParam(params, "ref");
			const inputRef = readToolStringParam(params, "inputRef");
			const element = readToolStringParam(params, "element");
			const { targetId, timeoutMs } = readOptionalTargetAndTimeout(params);
			return await trackedTabResult(await browserArmFileChooser(target, {
				paths: resolvedResult.paths,
				ref,
				inputRef,
				element,
				targetId,
				timeoutMs,
				profile,
				signal
			}), targetId);
		}
		case "dialog": {
			const accept = Boolean(params.accept);
			const promptText = readStringValue(params.promptText);
			const dialogId = readStringValue(params.dialogId);
			const { targetId, timeoutMs } = readOptionalTargetAndTimeout(params);
			return await trackedTabResult(await browserArmDialog(target, {
				accept,
				promptText,
				dialogId,
				targetId,
				timeoutMs,
				profile,
				signal
			}), targetId);
		}
		case "act": {
			const request = context.actRequest;
			if (!request) throw new Error("request required");
			if (!capabilities.actKinds.some((kind) => kind === request.kind)) throw new Error(`browser act kind ${JSON.stringify(request.kind)} is unavailable for this run`);
			return await executeActAction({
				request,
				baseUrl,
				profile,
				usesChromeMcp: isUserBrowserProfile,
				proxyRequest,
				signal,
				onTabActivity: touchTab,
				onTabClose: sessionTabs.untrack
			});
		}
		default: throw new Error(`Unknown action: ${action}`);
	}
}
//#endregion
//#region extensions/browser/src/browser-node-routing.ts
async function resolveBrowserNodeTarget(params) {
	const policy = params.config.gateway?.nodes?.browser;
	const mode = policy?.mode ?? "auto";
	const explicit = params.explicitTarget || Boolean(params.requestedNode?.trim());
	if (mode === "off") {
		if (explicit) throw new Error("Node browser proxy is disabled (gateway.nodes.browser.mode=off).");
		return null;
	}
	const requested = params.requestedNode?.trim() || policy?.node?.trim();
	if (mode === "manual" && !explicit && !requested) return null;
	if (!explicit && !requested) {
		const { isBrowserHostAvailable } = await import("./browser-host-availability-CaUjrJfY.mjs");
		if (await isBrowserHostAvailable(params.config, params.profile)) return null;
	}
	const browserNodes = (await params.nodes()).filter((node) => {
		if (params.requireConnected && !node.connected) return false;
		return node.caps?.includes("browser") || node.commands?.includes("browser.proxy");
	});
	if (browserNodes.length === 0) {
		if (explicit || requested) throw new Error("No connected browser-capable nodes.");
		return null;
	}
	if (requested) {
		const nodeId = resolveNodeIdFromList(browserNodes, requested, false, { allowCompactDisplayName: true });
		return browserNodes.find((node) => node.nodeId === nodeId) ?? null;
	}
	if (browserNodes.length === 1) return browserNodes[0] ?? null;
	if (explicit) throw new Error(`Multiple browser-capable nodes connected (${browserNodes.length}). Set gateway.nodes.browser.node or pass node=<id>.`);
	return null;
}
//#endregion
//#region extensions/browser/src/browser-tool.routing.ts
async function resolveBrowserToolNodeTarget(params) {
	if (params.allowHostControl === false) {
		if (params.target === "node" || params.requestedNode) throw new Error("Node browser control is disabled by sandbox policy.");
		return null;
	}
	const cfg = getRuntimeConfig();
	const policy = cfg.gateway?.nodes?.browser;
	const explicitTarget = params.target === "node";
	const requestedNode = params.requestedNode?.trim();
	if (policy?.mode === "off") {
		await resolveBrowserNodeTarget({
			nodes: () => [],
			config: cfg,
			requestedNode,
			explicitTarget
		});
		return null;
	}
	if (params.sandboxBridgeUrl?.trim() && !explicitTarget && !requestedNode) return null;
	if (params.target && !explicitTarget) return null;
	if (!explicitTarget && !requestedNode && !policy?.node?.trim() && (policy?.mode === "manual" || policy?.mode !== "auto" && !hasGatewayToolRoutingContext() && cfg.gateway?.mode !== "remote" && !cfg.gateway?.remote?.url?.trim() && !process.env.OPENCLAW_GATEWAY_URL?.trim())) return null;
	const node = await resolveBrowserNodeTarget({
		nodes: () => listNodes({}, params.signal),
		config: cfg,
		profile: params.profile,
		requestedNode,
		explicitTarget,
		requireConnected: true
	});
	params.signal?.throwIfAborted();
	return node ? {
		nodeId: node.nodeId,
		label: node.displayName ?? node.remoteIp ?? node.nodeId,
		commands: node.commands ?? [],
		pendingDeclaredCommands: node.pendingDeclaredCommands ?? []
	} : null;
}
function resolveBrowserBaseUrl(params) {
	const cfg = getRuntimeConfig();
	const resolved = resolveBrowserConfig(cfg.browser, cfg);
	const normalizedSandbox = params.sandboxBridgeUrl?.trim() ?? "";
	if ((params.target ?? (normalizedSandbox ? "sandbox" : "host")) === "sandbox") {
		if (!normalizedSandbox) throw new Error("Sandbox browser is unavailable. Enable agents.defaults.sandbox.browser.enabled or use target=\"host\" if allowed.");
		return normalizedSandbox.replace(/\/$/, "");
	}
	if (params.allowHostControl === false) throw new Error("Host browser control is disabled by sandbox policy.");
	if (!resolved.enabled) throw new Error("Browser control is disabled. Set browser.enabled=true in ~/.openclaw/openclaw.json.");
}
const DEFAULT_EXISTING_SESSION_MANAGE_TIMEOUT_MS = 45e3;
const EXISTING_SESSION_MANAGE_ACTIONS = /* @__PURE__ */ new Set([
	"status",
	"start",
	"stop",
	"profiles",
	"tabs",
	"open",
	"focus",
	"close"
]);
const PERSISTENT_TAB_ACTIONS = /* @__PURE__ */ new Set([
	"profiles",
	"tabs",
	"open",
	"focus",
	"close"
]);
function hasExistingSessionProfile(resolved) {
	return Object.keys(resolved.profiles).some((name) => {
		const candidate = resolveProfile(resolved, name);
		return candidate ? getBrowserProfileCapabilities(candidate).usesChromeMcp : false;
	});
}
function resolveBrowserToolTimeoutMs({ requestedTimeoutMs, action, isUserBrowserProfile, usesPersistentPlaywright, isNodeProxy, resolvedBrowser }) {
	if (requestedTimeoutMs !== void 0) return requestedTimeoutMs;
	if (EXISTING_SESSION_MANAGE_ACTIONS.has(action) && (isUserBrowserProfile || action === "profiles" && hasExistingSessionProfile(resolvedBrowser))) return DEFAULT_EXISTING_SESSION_MANAGE_TIMEOUT_MS;
	if (PERSISTENT_TAB_ACTIONS.has(action) && (usesPersistentPlaywright || isNodeProxy)) return resolvedBrowser.actionTimeoutMs;
}
//#endregion
//#region extensions/browser/src/browser-tool.lifecycle.ts
const unavailableSystemProfiles = (unavailableReason) => ({
	profiles: [],
	unavailableReason
});
/**
* Read importable system profiles from the host control server. Discovery must
* match where import runs (host-local), so it never uses a node proxy or the
* sandbox base URL. Other profile sources remain useful when host discovery
* is unavailable, so failures become an explicit degradation fact.
*/
async function readHostSystemProfiles(params) {
	if (params.allowHostControl === false) return unavailableSystemProfiles("Host system profile discovery is disabled by sandbox policy; enable host control to discover importable system profiles.");
	let hostBaseUrl;
	try {
		hostBaseUrl = resolveBrowserBaseUrl({
			target: "host",
			sandboxBridgeUrl: params.sandboxBridgeUrl,
			allowHostControl: params.allowHostControl
		});
	} catch {
		return unavailableSystemProfiles("Host browser control is unavailable; enable it and retry action=profiles target=\"host\".");
	}
	try {
		return {
			profiles: await browserSystemProfiles(hostBaseUrl, {
				timeoutMs: params.timeoutMs,
				signal: params.signal
			}),
			unavailableReason: void 0
		};
	} catch {
		params.signal?.throwIfAborted();
		return unavailableSystemProfiles("Host system profile discovery failed; retry action=profiles target=\"host\" after host browser control is available.");
	}
}
async function executeBrowserLifecycleAction({ action, input: params, baseUrl, profile, timeoutMs: toolTimeoutMs, proxyRequest, allowHostControl, sandboxBridgeUrl, signal }) {
	switch (action) {
		case "doctor": return jsonResult(await browserDoctor(proxyRequest ?? baseUrl, {
			profile,
			signal
		}));
		case "status":
		case "start":
		case "stop": {
			const options = {
				profile,
				timeoutMs: toolTimeoutMs,
				signal
			};
			if (action !== "status") await (action === "start" ? browserStart : browserStop)(proxyRequest ?? baseUrl, options);
			return jsonResult(await browserStatus(proxyRequest ?? baseUrl, options));
		}
		case "profiles": {
			const { profiles: systemProfiles, unavailableReason: systemProfilesUnavailable } = await readHostSystemProfiles({
				allowHostControl,
				sandboxBridgeUrl,
				timeoutMs: toolTimeoutMs,
				signal
			});
			const result = proxyRequest ? await proxyRequest({
				method: "GET",
				path: "/profiles",
				timeoutMs: toolTimeoutMs
			}) : { profiles: await browserProfiles(baseUrl, {
				timeoutMs: toolTimeoutMs,
				signal
			}) };
			return jsonResult({
				...result && typeof result === "object" ? result : { profiles: result },
				systemProfiles,
				...systemProfilesUnavailable ? { systemProfilesUnavailable } : {}
			});
		}
		case "importprofile": {
			if (proxyRequest) throw new Error("system profile import must run on the browser host");
			const domains = parseSystemProfileDomains(params.domains);
			return jsonResult(await browserImportProfile(baseUrl, {
				browser: normalizeOptionalString(params.browser) ?? "chrome",
				systemProfile: normalizeOptionalString(params.systemProfile) ?? "Default",
				into: normalizeOptionalString(params.into) ?? "imported",
				domains,
				signal
			}));
		}
		default: throw new Error(`Unknown action: ${String(action)}`);
	}
}
//#endregion
//#region extensions/browser/src/browser-tool.ts
function isBrowserRouteIdentifier(value, maxChars) {
	return typeof value === "string" && value.length > 0 && value.length <= maxChars && value.trim() === value;
}
function resolveBrowserTabIdentity(params) {
	if (params.baseUrl || !isBrowserRouteIdentifier(params.targetId, 128) || !isBrowserRouteIdentifier(params.profile, 128)) return;
	if (params.target === "node") return isBrowserRouteIdentifier(params.node, 256) ? {
		targetId: params.targetId,
		profile: params.profile,
		target: "node",
		node: params.node
	} : void 0;
	return {
		targetId: params.targetId,
		profile: params.profile,
		target: "host"
	};
}
function withBrowserTabDetails(result, identity) {
	const details = asNullableRecord(result.details);
	if (!identity || !details || details.ok === false || details.isError === true || Array.isArray(details.results) && details.results.some((entry) => asNullableRecord(entry)?.ok === false) || asNullableRecord(details.aborted)?.reason === "closed") return result;
	const url = readStringValue(details.url);
	const protocol = url ? URL.parse(url)?.protocol : void 0;
	const title = readStringValue(details.title);
	return {
		...result,
		details: {
			...details,
			browserTab: {
				...identity,
				...url && (protocol === "http:" || protocol === "https:") ? { url: truncateUtf16Safe(url, 2048) } : {},
				...title ? { title: truncateUtf16Safe(title, 512) } : {}
			}
		}
	};
}
const LEGACY_BROWSER_ACT_REQUEST_KEYS = [
	"kind",
	"actions",
	"stopOnError",
	"targetId",
	"ref",
	"doubleClick",
	"button",
	"modifiers",
	"x",
	"y",
	"text",
	"submit",
	"slowly",
	"key",
	"delayMs",
	"startRef",
	"endRef",
	"values",
	"fields",
	"width",
	"height",
	"timeMs",
	"textGone",
	"selector",
	"url",
	"loadState",
	"fn",
	"timeoutMs"
];
function readActRequestParam(params) {
	const requestParam = params.request;
	const nestedRequest = requestParam && typeof requestParam === "object" ? { ...requestParam } : void 0;
	if (!nestedRequest && !readToolStringParam(params, "kind")) return;
	const request = nestedRequest ?? {};
	const hasMismatchedKind = typeof request.kind === "string" && typeof params.kind === "string" && request.kind !== params.kind;
	for (const key of LEGACY_BROWSER_ACT_REQUEST_KEYS) {
		if (Object.hasOwn(request, key) || !Object.hasOwn(params, key)) continue;
		if (hasMismatchedKind && key !== "targetId") continue;
		request[key] = params[key];
	}
	return request;
}
function readToolTimeoutMs(params) {
	return readPositiveIntegerParam(params, "timeoutMs", { message: "timeoutMs must be a positive integer." });
}
function createBrowserTool(opts) {
	const { binding, capabilities, metadata } = createBrowserToolDefinition(opts, getRuntimeConfig);
	return {
		...metadata,
		execute: async (_toolCallId, args, signal) => {
			let params = binding ? applyBrowserTabToolBinding(args, binding) : args;
			const action = readToolStringParam(params, "action", { required: true });
			if (!capabilities.actions.some((candidate) => candidate === action)) throw new Error(`browser action ${JSON.stringify(action)} is unavailable for this run; use an available action such as snapshot, or select a managed browser profile in an unbound run.`);
			const dashboardName = readToolStringParam(params, "dashboard");
			let browserDashboard;
			if (dashboardName) {
				const operatorScopes = readGatewayToolOperatorScopes();
				const sessionScoped = operatorScopes !== void 0 && !operatorScopes.includes("operator.admin");
				const dashboardScopes = sessionScoped ? operatorScopes.includes("operator.write") ? ["operator.write"] : ["operator.sessions.write"] : ["operator.admin"];
				if (binding || !opts?.agentSessionKey || opts.allowHostControl === false || params.target && params.target !== "host" || params.node) throw new Error("Browser dashboard requires this session's unbound host-browser capability");
				const request = {
					sessionKey: opts.agentSessionKey,
					agentId: opts.agentId,
					name: dashboardName
				};
				if (params.profile !== void 0 || params.targetId !== void 0) throw new Error("A dashboard selector owns its browser profile and tab. Omit profile and targetId.");
				const callDashboard = async (method, resume = false) => {
					signal?.throwIfAborted();
					const dashboard = await callGatewayTool(sessionScoped ? "browser.dashboard.request" : "browser.request", { timeoutMs: readToolTimeoutMs(params) }, {
						...sessionScoped ? {
							sessionKey: request.sessionKey,
							agentId: request.agentId
						} : { target: "host" },
						method,
						path: "/dashboard",
						body: {
							...request,
							...resume ? { resume: true } : {}
						}
					}, {
						scopes: dashboardScopes,
						signal
					});
					signal?.throwIfAborted();
					return dashboard;
				};
				if (action === "close") return jsonResult({ browserDashboard: await callDashboard("DELETE") });
				if (action === "open") {
					if (params.targetUrl !== void 0 || params.url !== void 0) throw new Error("Update the dashboard widget URL with dashboard widget_put before opening it");
					return jsonResult({ browserDashboard: await callDashboard("POST", true) });
				}
				if ([
					"doctor",
					"status",
					"start",
					"stop",
					"profiles",
					"importprofile"
				].includes(action)) throw new Error("Use browser tab actions with a dashboard selector; open resumes it and close pauses it");
				const dashboard = await callDashboard("POST");
				browserDashboard = dashboard;
				if (!dashboard.browserTab) throw new Error(`Dashboard ${dashboardName} is paused. Use action=open with dashboard=${dashboardName} to resume it.`);
				params = applyBrowserTabToolBinding(params, {
					kind: "tab",
					tabId: 0,
					...dashboard.browserTab
				});
				if (sessionScoped) {
					if (![
						"tabs",
						"focus",
						"navigate",
						"snapshot",
						"screenshot",
						"act"
					].includes(action)) throw new Error("Session dashboards support tabs, focus, navigate, snapshot, screenshot and act; open resumes and close pauses them.");
					const stripSelectors = (input) => {
						if (!input) return;
						const { targetId: _targetId, profile: _profile, target: _target, node: _node, ...rest } = input;
						return rest;
					};
					const proxyRequest = Object.assign(async (call) => {
						return callGatewayTool("browser.dashboard.request", { timeoutMs: call.timeoutMs }, {
							sessionKey: request.sessionKey,
							agentId: request.agentId,
							dashboard: {
								name: dashboardName,
								instanceId: dashboard.instanceId
							},
							method: call.method,
							path: call.path,
							query: stripSelectors(call.query),
							body: stripSelectors(asNullableRecord(call.body) ?? void 0)
						}, {
							scopes: dashboardScopes,
							signal: call.signal ?? signal
						});
					}, {
						isHostFallbackActive: () => false,
						route: () => void 0
					});
					const result = await executeBrowserTabAction({
						action,
						params,
						actRequest: action === "act" ? readActRequestParam(params) : void 0,
						profile: dashboard.browserTab.profile,
						proxyRequest,
						capabilities,
						isUserBrowserProfile: false,
						boundTargetId: dashboard.browserTab.targetId,
						requestedTimeoutMs: readToolTimeoutMs(params),
						signal,
						opts,
						sessionTabs: {
							touch: async () => {},
							untrack: async () => {},
							trackOpened: async () => {
								throw new Error("Dashboard owns its context.");
							}
						},
						onTabActivity: () => {}
					});
					return {
						...result,
						details: {
							...asNullableRecord(result.details),
							browserDashboard: dashboard
						}
					};
				}
			}
			const requestedProfile = readToolStringParam(params, "profile");
			const requestedNode = readToolStringParam(params, "node");
			const requestedTimeoutMs = readToolTimeoutMs(params);
			let target = readToolStringParam(params, "target");
			const runtimeConfig = getRuntimeConfig();
			const resolvedBrowser = resolveBrowserConfig(runtimeConfig.browser, runtimeConfig);
			const effectiveProfile = requestedProfile ?? resolvedBrowser.defaultProfile;
			const resolvedProfile = resolveProfile(resolvedBrowser, effectiveProfile);
			const profileCapabilities = resolvedProfile ? getBrowserProfileCapabilities(resolvedProfile) : void 0;
			let profile = profileCapabilities?.usesChromeMcp ? effectiveProfile : requestedProfile;
			const configuredNode = runtimeConfig.gateway?.nodes?.browser?.node?.trim();
			if (requestedNode && target && target !== "node") throw new Error("node is only supported with target=\"node\".");
			if (action === "importprofile") {
				if (target === "sandbox" || target === "node" || requestedNode) throw new Error("system profile import must run on the host; omit target or use target=\"host\".");
				target = "host";
			}
			const isUserBrowserProfile = profileCapabilities?.usesChromeMcp === true;
			if (isUserBrowserProfile && target === "sandbox") throw new Error(`profile="${profile}" cannot use the sandbox browser; use target="host" or omit target.`);
			let nodeTarget = null;
			try {
				nodeTarget = await resolveBrowserToolNodeTarget({
					requestedNode: requestedNode ?? void 0,
					profile: requestedProfile,
					target,
					sandboxBridgeUrl: opts?.sandboxBridgeUrl,
					allowHostControl: opts?.allowHostControl,
					signal
				});
			} catch (error) {
				signal?.throwIfAborted();
				if (!(isUserBrowserProfile && !target && !requestedNode && !configuredNode)) throw error;
			}
			if (isUserBrowserProfile && !target && !requestedNode && !nodeTarget) target = "host";
			const baseUrl = nodeTarget ? void 0 : resolveBrowserBaseUrl({
				target: target === "node" ? void 0 : target,
				sandboxBridgeUrl: opts?.sandboxBridgeUrl,
				allowHostControl: opts?.allowHostControl
			});
			const allowAutomaticHostFallback = Boolean(nodeTarget && !target && !requestedNode && !configuredNode && opts?.allowHostControl !== false);
			const proxyRequest = nodeTarget ? createBrowserNodeProxyRequest({
				nodeTarget,
				allowAutomaticHostFallback,
				signal
			}) : null;
			if (proxyRequest) profile = requestedProfile;
			if (!proxyRequest && isUserBrowserProfile && [
				"requests",
				"errors",
				"text",
				"emulate"
			].includes(action)) throw new Error(`action=${action} is not supported for existing-session profiles; use action=snapshot to inspect this page, or select a managed browser profile for ${action}.`);
			const nodeRoute = nodeTarget ? createBrowserNodeSessionTabRoute(nodeTarget) : void 0;
			const toolTimeoutMs = resolveBrowserToolTimeoutMs({
				requestedTimeoutMs,
				action,
				isUserBrowserProfile,
				usesPersistentPlaywright: profileCapabilities?.usesPersistentPlaywright === true,
				isNodeProxy: proxyRequest !== null,
				resolvedBrowser
			});
			const sessionTabs = createBrowserToolSessionTabs({
				agentId: opts?.agentId,
				sessionKey: opts?.agentSessionKey,
				requestedProfile: profile,
				defaultProfile: resolvedBrowser.defaultProfile,
				baseUrl,
				nodeRoute,
				routeProfile: () => {
					const route = proxyRequest?.route();
					return route?.status === "resolved" ? route.profile : void 0;
				},
				isHostFallbackActive: proxyRequest?.isHostFallbackActive,
				registry: {
					touchSessionBrowserTab,
					trackSessionBrowserTab,
					untrackSessionBrowserTab
				}
			});
			switch (action) {
				case "doctor":
				case "status":
				case "start":
				case "stop":
				case "profiles":
				case "importprofile": return await executeBrowserLifecycleAction({
					action,
					input: params,
					baseUrl,
					profile,
					timeoutMs: toolTimeoutMs,
					proxyRequest,
					allowHostControl: opts?.allowHostControl,
					sandboxBridgeUrl: opts?.sandboxBridgeUrl,
					signal
				});
			}
			let tabIdentity;
			if (browserDashboard) {
				const { assertBrowserDashboardTargetCurrent } = await import("./browser-dashboard-D3rgqnI-.mjs");
				await assertBrowserDashboardTargetCurrent(browserDashboard, opts?.agentId, { signal });
			}
			const dispatchTabAction = () => executeBrowserTabAction({
				action,
				actRequest: action === "act" ? readActRequestParam(params) : void 0,
				params,
				baseUrl,
				profile,
				proxyRequest,
				nodeRoute,
				sessionTabs,
				capabilities,
				isUserBrowserProfile,
				toolTimeoutMs,
				requestedTimeoutMs,
				signal,
				opts,
				boundTargetId: binding ? binding.targetId : dashboardName ? readToolStringParam(params, "targetId") : void 0,
				onTabActivity: (targetId, openedProfile) => {
					const route = proxyRequest?.route();
					const onNode = proxyRequest && !proxyRequest.isHostFallbackActive();
					const routeProfile = onNode ? route?.status === "resolved" ? route.profile : void 0 : profile ?? resolvedBrowser.defaultProfile;
					tabIdentity = resolveBrowserTabIdentity({
						targetId,
						baseUrl,
						profile: openedProfile ?? routeProfile,
						target: onNode ? "node" : "host",
						node: onNode && route?.status === "resolved" ? nodeTarget?.nodeId : void 0
					});
				}
			});
			const dashboardTarget = browserDashboard;
			const result = dashboardTarget ? await withBrowserRequestScope({
				managedOnly: true,
				assertCurrent: async (admittedProfile) => (await import("./browser-dashboard-D3rgqnI-.mjs")).assertBrowserDashboardTargetCurrent(dashboardTarget, opts?.agentId, { signal }, admittedProfile)
			}, dispatchTabAction) : await dispatchTabAction();
			if (browserDashboard) return {
				...result,
				details: {
					...asNullableRecord(result.details),
					browserDashboard
				}
			};
			return [
				"open",
				"focus",
				"navigate",
				"screenshot",
				"snapshot",
				"text",
				"requests",
				"errors",
				"console",
				"emulate",
				"act"
			].includes(action) ? withBrowserTabDetails(result, tabIdentity) : result;
		}
	};
}
//#endregion
//#region extensions/browser/src/sdk-node-runtime.ts
async function withTimeout(work, timeoutMs, label) {
	const resolved = clampTimerTimeoutMs(timeoutMs);
	if (!resolved) return await work(void 0);
	const controller = new AbortController();
	const error = /* @__PURE__ */ new Error(`${label ?? "request"} timed out`);
	const timeout = createDeferred();
	const timer = setTimeout(() => {
		timeout.reject(error);
		controller.abort(error);
	}, resolved);
	timer.unref?.();
	try {
		return await Promise.race([work(controller.signal), timeout.promise]);
	} finally {
		clearTimeout(timer);
	}
}
//#endregion
//#region extensions/browser/src/gateway/browser-request-tab-scope.ts
const browserTabScopeSchema = strictObject({
	sessionKey: string().trim().min(1).max(512).refine((value) => Boolean(resolveBrowserSessionKey(value)), "Browser tab scope requires an agent-qualified session key"),
	referencedTabs: array(strictObject({
		target: _enum(["host", "node"]),
		node: string().trim().min(1).max(256).optional(),
		profile: string().trim().min(1).optional(),
		targetId: string().trim().min(1)
	}).refine((tab) => tab.target === "node" || tab.node === void 0)).max(64).optional()
});
/** Apply session membership only after the execution owner has completed the route. */
async function applyBrowserRequestTabScope(params) {
	params.assertCurrent();
	const listTabs = params.method === "GET" && params.path === "/tabs";
	const openTab = params.method === "POST" && params.path === "/tabs/open";
	const closeTab = params.method === "DELETE" && /^\/tabs\/[^/]+$/.test(params.path);
	const touchTab = params.method === "POST" && (params.path === "/navigate" || params.path === "/tabs/focus");
	if (!listTabs && !openTab && !closeTab && !touchTab) return params.result;
	const authority = {
		runtime: getOptionalBrowserStateRuntime() ?? void 0,
		assertCurrent: params.assertCurrent
	};
	const nodeRoute = params.nodeTarget ? createBrowserNodeSessionTabRoute(params.nodeTarget) : void 0;
	params.assertCurrent();
	const route = nodeRoute ?? { kind: "browser-control" };
	const profile = normalizeOptionalLowercaseString(params.profile);
	if (listTabs) {
		const result = asNullableRecord(params.result);
		if (!Array.isArray(result?.tabs)) return params.result;
		const tabs = result.tabs.flatMap((value) => {
			const tab = asNullableRecord(value);
			const targetId = normalizeOptionalString(tab?.targetId);
			return targetId ? [{
				value,
				targetId,
				tabId: normalizeOptionalString(tab?.tabId)
			}] : [];
		});
		const tracked = new Set(await filterTrackedSessionBrowserTabs({
			sessionKey: params.scope.sessionKey,
			route,
			profile,
			tabs,
			authority
		}));
		params.assertCurrent();
		const referenced = new Set((params.scope.referencedTabs ?? []).filter((tab) => {
			if (nodeRoute ? tab.target !== "node" || tab.node !== nodeRoute.nodeId : tab.target !== "host") return false;
			return normalizeOptionalLowercaseString(tab.profile ?? (nodeRoute ? !params.requestedProfile ? profile : void 0 : params.defaultProfile)) === profile;
		}).map((tab) => tab.targetId));
		return {
			...result,
			tabs: tabs.filter((tab) => tracked.has(tab) || referenced.has(tab.targetId) || Boolean(tab.tabId && referenced.has(tab.tabId))).map((tab) => tab.value)
		};
	}
	const sessionTabs = createBrowserToolSessionTabs({
		sessionKey: params.scope.sessionKey,
		requestedProfile: params.requestedProfile,
		defaultProfile: params.defaultProfile ?? "",
		nodeRoute,
		routeProfile: () => params.profile,
		registry: session_tab_registry_exports,
		authority
	});
	if (openTab) await sessionTabs.trackOpened(params.result, params.closeTab);
	else if (closeTab) await sessionTabs.untrack(decodeURIComponent(params.path.slice(6)));
	else if (touchTab) await sessionTabs.touch(normalizeOptionalString(asNullableRecord(params.body)?.targetId));
	params.assertCurrent();
	return params.result;
}
//#endregion
//#region extensions/browser/src/gateway/browser-request.ts
const logger = createSubsystemLogger("browser");
const dashboardRequestSchema = object({
	sessionKey: string().trim().min(1),
	agentId: string().trim().min(1).optional(),
	name: string().regex(/^[a-z0-9][a-z0-9._-]{0,63}$/),
	instanceId: string().min(1).optional()
});
async function handleBrowserGatewayRequest({ params, respond, context, client, signal: invocationSignal, hasCurrentClientAuthority }) {
	const reject = (...error) => respond(false, void 0, errorShape(...error));
	const typed = params;
	const methodRaw = (normalizeOptionalString(typed.method) ?? "").toUpperCase();
	const path = normalizeBrowserRequestPath(normalizeOptionalString(typed.path) ?? "");
	let query = typed.query && typeof typed.query === "object" ? typed.query : void 0;
	let body = typed.body;
	const timeoutMs = clampTimerTimeoutMs(typed.timeoutMs);
	const explicitNode = typed.target === "node";
	const requestedNode = normalizeOptionalString(typed.node);
	const connectionSignal = client?.connectionSignal;
	const requestSignal = invocationSignal && connectionSignal && invocationSignal !== connectionSignal ? AbortSignal.any([invocationSignal, connectionSignal]) : invocationSignal ?? connectionSignal;
	const isRequesterCurrent = () => !requestSignal?.aborted && !client?.invalidated && !client?.connectionSignal?.aborted && hasCurrentClientAuthority?.() !== false;
	const assertRequesterCurrent = () => {
		requestSignal?.throwIfAborted();
		if (!isRequesterCurrent()) throw new Error("Browser requester is no longer active");
	};
	const parsedTabScope = typed.tabScope === void 0 ? void 0 : browserTabScopeSchema.safeParse(typed.tabScope);
	if (parsedTabScope && (!parsedTabScope.success || typed.dashboard !== void 0 || path === "/dashboard")) {
		reject(ErrorCodes.INVALID_REQUEST, "tabScope requires a valid session tab scope and cannot be combined with dashboard");
		return;
	}
	const tabScope = parsedTabScope?.data;
	if (typed.target !== void 0 && typed.target !== "host" && !explicitNode || typed.node !== void 0 && (!explicitNode || !requestedNode || typeof typed.node !== "string" || typed.node.length > 256)) {
		reject(ErrorCodes.INVALID_REQUEST, "target must be \"host\" or \"node\"; node requires target=\"node\" and a nonempty selector of at most 256 characters");
		return;
	}
	if (!methodRaw || !path) {
		reject(ErrorCodes.INVALID_REQUEST, "method and path are required");
		return;
	}
	if (methodRaw !== "GET" && methodRaw !== "POST" && methodRaw !== "DELETE") {
		reject(ErrorCodes.INVALID_REQUEST, "method must be GET, POST, or DELETE");
		return;
	}
	if (path === "/dashboard") {
		if (typed.target === "node" || requestedNode) {
			reject(ErrorCodes.INVALID_REQUEST, "Browser dashboards use a local managed browser on the Gateway host");
			return;
		}
		const request = dashboardRequestSchema.extend({ resume: boolean().optional() }).safeParse(methodRaw === "GET" ? query : body);
		if (!request.success) {
			reject(ErrorCodes.INVALID_REQUEST, "Browser dashboard requires sessionKey and a stable widget name");
			return;
		}
		try {
			const { stopBrowserDashboard, inspectBrowserDashboard, requestBrowserDashboard } = await import("./browser-dashboard-D3rgqnI-.mjs");
			respond(true, await (methodRaw === "DELETE" ? stopBrowserDashboard : methodRaw === "GET" ? inspectBrowserDashboard : requestBrowserDashboard)(request.data, {
				signal: requestSignal,
				assertCurrent: assertRequesterCurrent
			}));
		} catch (error) {
			reject(ErrorCodes.INVALID_REQUEST, String(error));
		}
		return;
	}
	let assertDashboardCurrent;
	if (typed.dashboard !== void 0) {
		const scope = dashboardRequestSchema.safeParse(typed.dashboard);
		if (!scope.success || explicitNode || requestedNode) {
			reject(ErrorCodes.INVALID_REQUEST, "Dashboard requests require a valid local dashboard identity");
			return;
		}
		try {
			const { inspectBrowserDashboard, assertBrowserDashboardTargetCurrent } = await import("./browser-dashboard-D3rgqnI-.mjs");
			const authority = {
				signal: requestSignal,
				assertCurrent: assertRequesterCurrent
			};
			const dashboard = await inspectBrowserDashboard(scope.data, authority);
			const tab = dashboard.browserTab;
			if (!tab || dashboard.paused) throw new Error("Dashboard browser is paused or unavailable. Resume the dashboard first.");
			if (path === "/tabs/open" || path === "/tabs/action" || path === "/start" || path === "/stop" || path === "/reset-profile" || path.startsWith("/profiles") || path.startsWith("/system-")) throw new Error("Use the dashboard controls to open or stop its retained tab");
			if (methodRaw === "DELETE" && path.startsWith("/tabs/") && decodeURIComponent(path.slice(6)) !== tab.targetId) throw new Error("Dashboard request cannot address another browser tab");
			const binding = {
				kind: "tab",
				tabId: 0,
				...tab
			};
			query = {
				...applyBrowserTabToolBinding(query ?? {}, binding),
				managedOnly: true
			};
			const bodyRecord = asNullableRecord(body);
			if (bodyRecord || methodRaw === "POST") body = applyBrowserTabToolBinding(bodyRecord ?? {}, binding);
			assertDashboardCurrent = (profile) => assertBrowserDashboardTargetCurrent(dashboard, scope.data.agentId, authority, profile);
			await assertDashboardCurrent();
		} catch (error) {
			reject(ErrorCodes.INVALID_REQUEST, String(error));
			return;
		}
	}
	const cfg = getRuntimeConfig();
	const configuredNode = normalizeOptionalString(cfg.gateway?.nodes?.browser?.node);
	const forceHostLocal = Boolean(assertDashboardCurrent) || isBrowserHostLocalRoute(methodRaw, path);
	if (forceHostLocal && explicitNode) {
		reject(ErrorCodes.INVALID_REQUEST, "this browser route must run on the Gateway host");
		return;
	}
	let nodeTarget = null;
	if (!forceHostLocal && typed.target !== "host") try {
		nodeTarget = await resolveBrowserNodeTarget({
			nodes: () => context.nodeRegistry.listConnected(),
			config: cfg,
			profile: resolveRequestedBrowserProfile({
				query,
				body
			}),
			explicitTarget: explicitNode,
			requestedNode
		});
		assertRequesterCurrent();
	} catch (err) {
		reject(ErrorCodes.UNAVAILABLE, String(err));
		return;
	}
	if (nodeTarget && path === "/screencast") {
		reject(ErrorCodes.INVALID_REQUEST, "browser screencast is not available over a node proxy", { details: {
			code: "SCREENCAST_UNSUPPORTED",
			reason: "node"
		} });
		return;
	}
	if (nodeTarget && isPersistentBrowserProfileMutation(methodRaw, path)) {
		reject(ErrorCodes.INVALID_REQUEST, "browser.request cannot mutate persistent browser profiles over a node proxy");
		return;
	}
	let preparedUpload = null;
	let proxyCommand = BROWSER_PROXY_COMMAND;
	if (nodeTarget) {
		if (isBrowserProxyUploadRequest({
			method: methodRaw,
			path,
			body
		}) && !nodeTarget.commands?.includes("browser.proxy.upload.v1")) {
			const message = browserProxyUploadUnavailableMessage(nodeTarget.declaredCommands);
			if (explicitNode || configuredNode) {
				reject(ErrorCodes.UNAVAILABLE, message);
				return;
			}
			logger.warn(`browser node ${nodeTarget.displayName ?? nodeTarget.nodeId} lacks ${BROWSER_PROXY_UPLOAD_COMMAND}; falling back to Gateway host`);
			nodeTarget = null;
		}
	}
	if (nodeTarget) {
		try {
			assertRequesterCurrent();
			preparedUpload = await prepareBrowserProxyUploadRequest({
				method: methodRaw,
				path,
				body,
				signal: requestSignal
			});
			assertRequesterCurrent();
		} catch (err) {
			const message = err instanceof Error ? err.message : String(err);
			reject(ErrorCodes.INVALID_REQUEST, message);
			return;
		}
		if (preparedUpload.upload) proxyCommand = BROWSER_PROXY_UPLOAD_COMMAND;
	}
	if (nodeTarget && preparedUpload) {
		const resolvedNodeTarget = nodeTarget;
		const allowlist = resolveNodeCommandAllowlist(cfg, nodeTarget);
		const allowed = isNodeCommandAllowed({
			command: proxyCommand,
			declaredCommands: nodeTarget.commands,
			allowlist
		});
		if (!allowed.ok) {
			const platform = nodeTarget.platform ?? "unknown";
			const hint = `node command not allowed: ${allowed.reason} (platform: ${platform}, command: ${proxyCommand})`;
			reject(ErrorCodes.INVALID_REQUEST, hint, { details: {
				reason: allowed.reason,
				command: proxyCommand
			} });
			return;
		}
		const { proxyTimeoutMs, nodeInvokeTimeoutMs } = resolveBrowserProxyTimeouts(timeoutMs);
		const invokeProxy = async (proxyRequest, command = BROWSER_PROXY_COMMAND) => {
			assertRequesterCurrent();
			const invoked = await context.nodeRegistry.invoke({
				nodeId: resolvedNodeTarget.nodeId,
				command,
				params: {
					...proxyRequest,
					timeoutMs: proxyTimeoutMs,
					errorEnvelope: BROWSER_PROXY_ERROR_ENVELOPE
				},
				timeoutMs: nodeInvokeTimeoutMs,
				signal: requestSignal,
				isDispatchAuthorized: isRequesterCurrent,
				idempotencyKey: crypto.randomUUID()
			});
			assertRequesterCurrent();
			return invoked;
		};
		const proxyParams = {
			method: methodRaw,
			path,
			query,
			body: preparedUpload.body,
			upload: preparedUpload.upload,
			profile: resolveRequestedBrowserProfile({
				query,
				body
			})
		};
		let res;
		try {
			res = await invokeProxy(proxyParams, proxyCommand);
		} catch (error) {
			reject(ErrorCodes.UNAVAILABLE, String(error));
			return;
		}
		if (!explicitNode && !configuredNode && isBrowserControlHostUnavailableError(res.error) && !res.ok) logger.warn(`browser node ${nodeTarget.displayName ?? nodeTarget.nodeId} control host unavailable; falling back to Gateway host`);
		else {
			if (!respondUnavailableOnNodeInvokeError(respond, res)) return;
			const payload = res.payloadJSON ? parseGatewayPayload(res.payloadJSON) : res.payload;
			const failure = parseBrowserProxyFailure(payload);
			if (failure) {
				const { status, body: errorBody } = failure.error;
				reject(status >= 500 ? ErrorCodes.UNAVAILABLE : ErrorCodes.INVALID_REQUEST, errorBody.error, { details: errorBody });
				return;
			}
			const proxy = payload && typeof payload === "object" ? payload : null;
			if (!proxy || !("result" in proxy)) {
				reject(ErrorCodes.UNAVAILABLE, "browser proxy failed");
				return;
			}
			try {
				const result = await persistBrowserProxyResultFiles(proxy.result, proxy.files);
				assertRequesterCurrent();
				const resolvedRoute = parseBrowserProxyRoute(proxy);
				const scopedResult = tabScope ? await applyBrowserRequestTabScope({
					scope: tabScope,
					method: methodRaw,
					path,
					body,
					result,
					nodeTarget,
					profile: resolvedRoute?.status === "resolved" ? resolvedRoute.profile : resolveRequestedBrowserProfile({
						query,
						body
					}),
					requestedProfile: resolveRequestedBrowserProfile({
						query,
						body
					}),
					assertCurrent: assertRequesterCurrent,
					closeTab: async (targetId, profile) => {
						const closed = await invokeProxy({
							method: "DELETE",
							path: `/tabs/${encodeURIComponent(targetId)}`,
							query: { targetIdMode: "raw" },
							profile
						});
						const closePayload = closed.payloadJSON ? parseGatewayPayload(closed.payloadJSON) : closed.payload;
						if (!closed.ok || parseBrowserProxyFailure(closePayload) || !asNullableRecord(closePayload)?.result) throw new Error("Failed to close newly opened browser node tab");
					}
				}) : result;
				assertRequesterCurrent();
				respond(true, scopedResult);
			} catch (error) {
				reject(ErrorCodes.UNAVAILABLE, tabScope ? String(error) : "browser proxy file transfer failed");
			}
			return;
		}
	}
	const ready = await startBrowserControlServiceFromConfig();
	if (!ready) {
		reject(ErrorCodes.UNAVAILABLE, await describeBrowserControlUnavailable());
		return;
	}
	let dispatcher;
	try {
		dispatcher = createBrowserRouteDispatcher(createBrowserControlContext());
	} catch (err) {
		reject(ErrorCodes.UNAVAILABLE, String(err));
		return;
	}
	const requesterSignal = connectionSignal;
	const requester = client && requesterSignal ? {
		connId: client.connId,
		signal: requesterSignal,
		isCurrent: () => client.invalidated !== true && !requesterSignal.aborted && hasCurrentClientAuthority?.() !== false
	} : void 0;
	let resolvedProfile;
	const assertCurrent = async (profile) => {
		assertRequesterCurrent();
		if (profile) resolvedProfile = profile.name;
		await assertDashboardCurrent?.(profile);
		assertRequesterCurrent();
	};
	const dispatch = (timeoutSignal) => dispatcher.dispatch({
		method: methodRaw,
		path,
		query,
		body,
		signal: timeoutSignal && requestSignal ? AbortSignal.any([timeoutSignal, requestSignal]) : timeoutSignal ?? requestSignal,
		...requester ? { requester } : {},
		assertCurrent
	});
	let result;
	try {
		await assertCurrent();
		result = timeoutMs ? await withTimeout(dispatch, timeoutMs, "browser request") : await dispatch();
	} catch (err) {
		reject(ErrorCodes.UNAVAILABLE, String(err));
		return;
	}
	if (result.status >= 400) {
		const message = result.body && typeof result.body === "object" && "error" in result.body ? String(result.body.error) : `browser request failed (${result.status})`;
		reject(result.status >= 500 ? ErrorCodes.UNAVAILABLE : ErrorCodes.INVALID_REQUEST, message, { details: result.body });
		return;
	}
	try {
		respond(true, tabScope ? await applyBrowserRequestTabScope({
			scope: tabScope,
			method: methodRaw,
			path,
			body,
			result: result.body,
			profile: resolvedProfile,
			requestedProfile: resolveRequestedBrowserProfile({
				query,
				body
			}),
			defaultProfile: ready.resolved.defaultProfile,
			assertCurrent: assertRequesterCurrent,
			closeTab: async (targetId, profile) => {
				assertRequesterCurrent();
				const closed = await dispatcher.dispatch({
					method: "DELETE",
					path: `/tabs/${encodeURIComponent(targetId)}`,
					query: {
						profile,
						targetIdMode: "raw"
					},
					signal: requestSignal,
					...requester ? { requester } : {},
					assertCurrent
				});
				assertRequesterCurrent();
				if (closed.status >= 400) throw new Error(`Failed to close newly opened browser tab (${closed.status})`);
			}
		}) : result.body);
	} catch (error) {
		reject(ErrorCodes.UNAVAILABLE, String(error));
	}
}
const browserHandlers = { "browser.request": handleBrowserGatewayRequest };
//#endregion
export { createBrowserTool as i, handleBrowserGatewayRequest as n, withTimeout as r, browserHandlers as t };