openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
1,388 lines • 50.6 kB
JavaScript
import { a as normalizeLowercaseStringOrEmpty, c as normalizeOptionalString, p as readStringValue } from "./string-coerce-mnp54Vah.js";
import { f as clampTimerTimeoutMs } from "./number-coercion-CJQ8TR--.js";
import { i as getRuntimeConfig } from "./io-Gi7-pyU-.js";
import { u as saveMediaBuffer } from "./store-C9M_0A1p.js";
import { b as readStringParam, c as imageResultFromFile, g as readPositiveIntegerParam, l as jsonResult, m as readNonNegativeIntegerParam } from "./common-C4yy9V-D.js";
import { t as callGatewayTool } from "./gateway-Dq9H8-iz.js";
import { a as selectDefaultNodeFromList, i as resolveNodeIdFromList, t as listNodes } from "./nodes-utils-CTBPI5uk.js";
import { in as errorShape, rn as ErrorCodes } from "./schema-BwaBORnA.js";
import "./number-runtime-DBLVDypr.js";
import { i as wrapExternalContent } from "./external-content-pX-Pk1Iu.js";
import "./string-coerce-runtime-CEGJWkQ_.js";
import { n as startLazyPluginServiceModule } from "./plugin-runtime-Dw_Zc32A.js";
import { o as resolveNodeCommandAllowlist, r as isNodeCommandAllowed } from "./node-command-policy-DYPdfWjH.js";
import { i as safeParseJson, n as respondUnavailableOnNodeInvokeError } from "./nodes.helpers-DeAuiiQp.js";
import { t as describeImageFile } from "./runtime-sq-ErJqP.js";
import "./param-readers-HkqGooLN.js";
import { t as BrowserToolSchema } from "./browser-tool.schema-tt4NcGdm.js";
import { n as withTimeout } from "./sdk-security-runtime-D1sDHmUD.js";
import { L as DEFAULT_AI_SNAPSHOT_MAX_CHARS } from "./cdp.helpers-CTQ-1blZ.js";
import { c as resolveExistingUploadPaths, i as resolveProfile, n as resolveBrowserConfig } from "./config-C2rsUqn7.js";
import "./sdk-setup-tools-Bm-WxrMN.js";
import { _ as browserStop, a as untrackSessionBrowserTab, d as browserOpenTab, f as browserProfiles, g as browserStatus, h as browserStart, i as trackSessionBrowserTab, l as browserDoctor, m as browserSnapshot, o as browserCloseTab, r as touchSessionBrowserTab, u as browserFocusTab, y as browserTabs } from "./session-tab-registry-BG1AGKBy.js";
import { a as applyBrowserProxyPaths, c as browserPdfSave, d as browserArmFileChooser, f as browserNavigate, i as resolveRequestedBrowserProfile, l as browserAct, n as isPersistentBrowserProfileMutation, o as persistBrowserProxyFiles, p as browserScreenshotAction, s as browserConsoleMessages, u as browserArmDialog } from "./core-api-Cugd5en1.js";
import { r as getBrowserProfileCapabilities } from "./target-id-CwSK7UJx.js";
import { n as normalizeBrowserScreenshot } from "./routes-BHi6gQTW.js";
import { t as createBrowserRouteDispatcher } from "./dispatcher-BGpFk5sG.js";
import { n as createBrowserControlContext } from "./plugin-enabled-QojYdRCs.js";
import { t as startBrowserControlServiceFromConfig } from "./control-service-CDT0rmx7.js";
import { readFile } from "node:fs/promises";
import crypto from "node:crypto";
//#region extensions/browser/src/browser-tool.runtime.ts
/**
* Runtime dependency barrel for the Browser agent tool.
*
* Kept separate from browser-tool.ts so tests can mock the tool boundary while
* production still imports SDK helpers and browser client actions lazily.
*/
/** Resolve global image downscaling for screenshots returned to agent tools. */
function resolveRuntimeImageSanitization() {
const configured = getRuntimeConfig().agents?.defaults?.imageMaxDimensionPx;
if (typeof configured !== "number" || !Number.isFinite(configured)) return;
return { maxDimensionPx: Math.max(1, Math.floor(configured)) };
}
//#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, deps) {
const maxDimensionPx = ctx.imageSanitization?.maxDimensionPx;
if (typeof maxDimensionPx !== "number" || !Number.isFinite(maxDimensionPx)) return ctx.filePath;
const source = await readFile(ctx.filePath);
const normalized = await deps.normalizeBrowserScreenshot(source, { maxSide: Math.max(1, Math.floor(maxDimensionPx)) });
if (normalized.buffer === source) return ctx.filePath;
return (await deps.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, deps) {
const filePath = await resolveImageUnderstandingFilePath(ctx, deps);
const described = await deps.describeImageFile({
filePath,
cfg: ctx.cfg,
prompt: DEFAULT_BROWSER_SCREENSHOT_DESCRIPTION_PROMPT,
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;
const lines = text.split("\n");
let changed = false;
for (let i = 0; i < lines.length; i += 1) {
const line = lines[i];
const leading = line.length - line.trimStart().length;
const rest = line.slice(leading);
if (/^MEDIA:/i.test(rest)) {
lines[i] = `${line.slice(0, leading)}[neutralized] ${rest}`;
changed = true;
}
}
return changed ? lines.join("\n") : text;
}
//#endregion
//#region extensions/browser/src/browser-tool.actions.ts
const browserToolActionDeps = {
browserAct,
browserConsoleMessages,
browserSnapshot,
browserTabs,
getRuntimeConfig,
imageResultFromFile
};
const BROWSER_ACT_REQUEST_TIMEOUT_SLACK_MS = 5e3;
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 supportsBrowserActTimeout(request) {
switch (request.kind) {
case "click":
case "type":
case "hover":
case "scrollIntoView":
case "drag":
case "select":
case "fill":
case "evaluate":
case "wait": return true;
default: return false;
}
}
function existingSessionRejectsActTimeout(request) {
switch (request.kind) {
case "type":
case "hover":
case "scrollIntoView":
case "drag":
case "select":
case "fill":
case "evaluate": return true;
default: return false;
}
}
function usesExistingSessionProfile(profileName) {
const cfg = browserToolActionDeps.getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
const profile = resolveProfile(resolved, profileName ?? resolved.defaultProfile);
return profile ? getBrowserProfileCapabilities(profile).usesChromeMcp : false;
}
function withConfiguredActTimeout(request, profileName) {
const typedRequest = request;
if (normalizePositiveTimeoutMs(typedRequest.timeoutMs) !== void 0) return request;
if (!supportsBrowserActTimeout(request)) return request;
if (existingSessionRejectsActTimeout(request) && usesExistingSessionProfile(profileName)) return request;
const configuredTimeout = normalizePositiveTimeoutMs(browserToolActionDeps.getRuntimeConfig().browser?.actionTimeoutMs) ?? 6e4;
return {
...typedRequest,
timeoutMs: configuredTimeout
};
}
function resolveActProxyTimeoutMs(request) {
const candidateTimeouts = [];
const explicitTimeout = normalizePositiveTimeoutMs(request.timeoutMs);
if (explicitTimeout !== void 0) candidateTimeouts.push(explicitTimeout + BROWSER_ACT_REQUEST_TIMEOUT_SLACK_MS);
if (request.kind === "wait") {
const waitDuration = normalizeNonNegativeDurationMs(request.timeMs);
if (waitDuration !== void 0) candidateTimeouts.push(waitDuration + BROWSER_ACT_REQUEST_TIMEOUT_SLACK_MS);
}
return candidateTimeouts.length ? Math.max(...candidateTimeouts) : void 0;
}
function formatAgentTab(tab) {
if (!tab || typeof tab !== "object") return { value: tab };
const source = tab;
const targetId = readStringValue(source.targetId);
const tabId = readStringValue(source.tabId);
const label = readStringValue(source.label);
const suggestedTargetId = readStringValue(source.suggestedTargetId) ?? label ?? tabId ?? targetId;
return {
...suggestedTargetId ? { suggestedTargetId } : {},
...tabId ? { tabId } : {},
...label ? { label } : {},
title: source.title,
url: source.url,
type: source.type,
...targetId ? { targetId } : {},
...source.wsUrl ? { wsUrl: source.wsUrl } : {}
};
}
function wrapBrowserExternalJson(params) {
return {
wrappedText: wrapExternalContent(JSON.stringify(params.payload, (_key, value) => typeof value === "string" ? neutralizeMediaDirectives(value) : value, 2), {
source: "browser",
includeWarning: params.includeWarning ?? true
}),
safeDetails: {
ok: true,
externalContent: {
untrusted: true,
source: "browser",
kind: params.kind,
wrapped: true
}
}
};
}
function formatTabsToolResult(tabs) {
const formattedTabs = tabs.map((tab) => formatAgentTab(tab));
const wrapped = wrapBrowserExternalJson({
kind: "tabs",
payload: { tabs: formattedTabs },
includeWarning: false
});
return {
content: [{
type: "text",
text: wrapped.wrappedText
}],
details: {
...wrapped.safeDetails,
tabCount: tabs.length,
tabs: formattedTabs
}
};
}
function formatConsoleToolResult(result) {
const wrapped = wrapBrowserExternalJson({
kind: "console",
payload: result,
includeWarning: false
});
return {
content: [{
type: "text",
text: wrapped.wrappedText
}],
details: {
...wrapped.safeDetails,
targetId: readStringValue(result.targetId),
url: readStringValue(result.url),
messageCount: Array.isArray(result.messages) ? result.messages.length : void 0
}
};
}
function isChromeStaleTargetError(profile, err) {
if (!profile) return false;
if (profile === "user") {
const msg = String(err);
return msg.includes("404:") && msg.includes("tab not found");
}
const cfg = browserToolActionDeps.getRuntimeConfig();
const browserProfile = resolveProfile(resolveBrowserConfig(cfg.browser, cfg), profile);
if (!browserProfile || !getBrowserProfileCapabilities(browserProfile).usesChromeMcp) return false;
const msg = String(err);
return msg.includes("404:") && msg.includes("tab not found");
}
function stripTargetIdFromActRequest(request) {
if (!normalizeOptionalString(request.targetId)) return null;
const retryRequest = { ...request };
delete retryRequest.targetId;
return retryRequest;
}
function canRetryChromeActWithoutTargetId(request) {
const typedRequest = request;
const kind = typeof typedRequest.kind === "string" ? typedRequest.kind : typeof typedRequest.action === "string" ? typedRequest.action : "";
return kind === "hover" || kind === "scrollIntoView" || kind === "wait";
}
function isAriaRefsUnsupportedError(err) {
const msg = String(err).toLowerCase();
return msg.includes("refs=aria") && msg.includes("not support");
}
function withRoleRefsFallback(snapshotQuery) {
return {
...snapshotQuery,
refs: "role"
};
}
async function executeTabsAction(params) {
const { baseUrl, profile, timeoutMs, proxyRequest } = params;
if (proxyRequest) return formatTabsToolResult((await proxyRequest({
method: "GET",
path: "/tabs",
profile,
timeoutMs
})).tabs ?? []);
return formatTabsToolResult(await browserToolActionDeps.browserTabs(baseUrl, {
profile,
timeoutMs
}));
}
/** Execute and format browser snapshots for agent consumption. */
async function executeSnapshotAction(params) {
const { input, baseUrl, profile, proxyRequest } = params;
const snapshotDefaults = browserToolActionDeps.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" ? "efficient" : !formatExplicit && format !== "aria" && 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 = format === "ai" ? hasMaxChars ? maxChars : mode === "efficient" ? void 0 : DEFAULT_AI_SNAPSHOT_MAX_CHARS : hasMaxChars ? maxChars : 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) => proxyRequest ? await proxyRequest({
method: "GET",
path: "/snapshot",
profile,
query,
timeoutMs: snapshotTimeoutMs
}) : await browserToolActionDeps.browserSnapshot(baseUrl, {
...query,
profile
});
let snapshot;
try {
snapshot = await readSnapshot(snapshotQuery);
} catch (err) {
if (refs !== "aria" || !isAriaRefsUnsupportedError(err)) throw err;
refsFallback = "role";
snapshot = await readSnapshot(withRoleRefsFallback(snapshotQuery));
}
params.onTabActivity?.(readStringValue(snapshot.targetId) ?? targetId);
if (snapshot.format === "ai") {
const dialogStateFields = {
...snapshot.blockedByDialog ? { blockedByDialog: true } : {},
...snapshot.browserState !== void 0 ? { browserState: snapshot.browserState } : {}
};
if (snapshot.blockedByDialog) {
const wrapped = wrapBrowserExternalJson({
kind: "snapshot",
payload: {
format: snapshot.format,
targetId: snapshot.targetId,
url: snapshot.url,
...dialogStateFields
}
});
return {
content: [{
type: "text",
text: wrapped.wrappedText
}],
details: {
...wrapped.safeDetails,
format: snapshot.format,
targetId: snapshot.targetId,
url: snapshot.url,
...dialogStateFields
}
};
}
const wrappedSnapshot = wrapExternalContent(neutralizeMediaDirectives(snapshot.snapshot ?? ""), {
source: "browser",
includeWarning: true
});
const safeDetails = {
ok: true,
format: snapshot.format,
targetId: snapshot.targetId,
url: snapshot.url,
truncated: snapshot.truncated,
stats: snapshot.stats,
refs: snapshot.refs ? Object.keys(snapshot.refs).length : void 0,
labels: snapshot.labels,
labelsCount: snapshot.labelsCount,
labelsSkipped: snapshot.labelsSkipped,
imagePath: snapshot.imagePath,
imageType: snapshot.imageType,
refsFallback,
...dialogStateFields,
externalContent: {
untrusted: true,
source: "browser",
kind: "snapshot",
format: "ai",
wrapped: true
}
};
if (labels && snapshot.imagePath) return await browserToolActionDeps.imageResultFromFile({
label: "browser:snapshot",
path: snapshot.imagePath,
extraText: wrappedSnapshot,
details: safeDetails,
imageSanitization: resolveRuntimeImageSanitization()
});
return {
content: [{
type: "text",
text: wrappedSnapshot
}],
details: safeDetails
};
}
{
const wrapped = wrapBrowserExternalJson({
kind: "snapshot",
payload: snapshot
});
return {
content: [{
type: "text",
text: wrapped.wrappedText
}],
details: {
...wrapped.safeDetails,
format: "aria",
targetId: snapshot.targetId,
url: snapshot.url,
nodeCount: snapshot.nodes.length,
...snapshot.blockedByDialog ? { blockedByDialog: true } : {},
...snapshot.browserState !== void 0 ? { browserState: snapshot.browserState } : {},
externalContent: {
untrusted: true,
source: "browser",
kind: "snapshot",
format: "aria",
wrapped: true
}
}
};
}
}
/** Execute browser console retrieval and wrap page-controlled messages. */
async function executeConsoleAction(params) {
const { input, baseUrl, profile, proxyRequest } = params;
const level = normalizeOptionalString(input.level);
const targetId = normalizeOptionalString(input.targetId);
if (proxyRequest) return formatConsoleToolResult(await proxyRequest({
method: "GET",
path: "/console",
profile,
query: {
level,
targetId
}
}));
return formatConsoleToolResult(await browserToolActionDeps.browserConsoleMessages(baseUrl, {
level,
targetId,
profile
}));
}
/** Execute browser actions with profile-aware timeout defaults and stale-tab recovery. */
async function executeActAction(params) {
const { request, baseUrl, profile, proxyRequest } = params;
const effectiveRequest = withConfiguredActTimeout(request, profile);
try {
const result = proxyRequest ? await proxyRequest({
method: "POST",
path: "/act",
profile,
body: effectiveRequest,
timeoutMs: resolveActProxyTimeoutMs(effectiveRequest)
}) : await browserToolActionDeps.browserAct(baseUrl, effectiveRequest, { profile });
params.onTabActivity?.(readStringValue(result.targetId) ?? readStringValue(effectiveRequest.targetId));
return jsonResult(result);
} catch (err) {
if (isChromeStaleTargetError(profile, err)) {
const retryRequest = stripTargetIdFromActRequest(effectiveRequest);
const tabs = proxyRequest ? (await proxyRequest({
method: "GET",
path: "/tabs",
profile
})).tabs ?? [] : await browserToolActionDeps.browserTabs(baseUrl, { profile }).catch(() => []);
if (retryRequest && canRetryChromeActWithoutTargetId(effectiveRequest) && tabs.length === 1) try {
const retryResult = proxyRequest ? await proxyRequest({
method: "POST",
path: "/act",
profile,
body: retryRequest,
timeoutMs: resolveActProxyTimeoutMs(retryRequest)
}) : await browserToolActionDeps.browserAct(baseUrl, retryRequest, { profile });
params.onTabActivity?.(readStringValue(retryResult.targetId) ?? readStringValue(retryRequest.targetId));
return jsonResult(retryResult);
} catch {}
if (!tabs.length) throw new Error(`No browser tabs found for profile="${profile}". 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="${profile}" and use one of the returned targetIds.`, { cause: err });
}
throw err;
}
}
//#endregion
//#region extensions/browser/src/browser-tool.ts
/**
* Browser agent tool registration.
*
* Builds the model-facing browser tool, chooses sandbox/host/node routing, and
* maps high-level actions onto browser control client calls.
*/
const browserToolDeps = {
browserAct,
browserArmDialog,
browserArmFileChooser,
browserCloseTab,
browserDoctor,
browserFocusTab,
browserNavigate,
browserOpenTab,
browserPdfSave,
browserProfiles,
browserScreenshotAction,
browserStart,
browserStatus,
browserStop,
describeImageFile,
getRuntimeConfig,
imageResultFromFile,
listNodes,
callGatewayTool,
normalizeBrowserScreenshot,
saveMediaBuffer,
touchSessionBrowserTab,
trackSessionBrowserTab,
untrackSessionBrowserTab
};
function readOptionalTargetAndTimeout(params) {
return {
targetId: normalizeOptionalString(params.targetId),
timeoutMs: readPositiveIntegerParam(params, "timeoutMs", { message: "timeoutMs must be a positive integer." })
};
}
function readTargetUrlParam(params) {
return readStringParam(params, "targetUrl") ?? readStringParam(params, "url", {
required: true,
label: "targetUrl"
});
}
const LEGACY_BROWSER_ACT_REQUEST_KEYS = [
"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;
if (requestParam && typeof requestParam === "object") return requestParam;
const kind = readStringParam(params, "kind");
if (!kind) return;
const request = { kind };
for (const key of LEGACY_BROWSER_ACT_REQUEST_KEYS) {
if (!Object.hasOwn(params, key)) continue;
request[key] = params[key];
}
return request;
}
const DEFAULT_BROWSER_PROXY_TIMEOUT_MS = 2e4;
const BROWSER_PROXY_GATEWAY_TIMEOUT_SLACK_MS = 5e3;
function isBrowserNode$1(node) {
const caps = Array.isArray(node.caps) ? node.caps : [];
const commands = Array.isArray(node.commands) ? node.commands : [];
return caps.includes("browser") || commands.includes("browser.proxy");
}
async function resolveBrowserNodeTarget$1(params) {
const policy = browserToolDeps.getRuntimeConfig().gateway?.nodes?.browser;
const mode = policy?.mode ?? "auto";
if (mode === "off") {
if (params.target === "node" || params.requestedNode) throw new Error("Node browser proxy is disabled (gateway.nodes.browser.mode=off).");
return null;
}
if (params.sandboxBridgeUrl?.trim() && params.target !== "node" && !params.requestedNode) return null;
if (params.target && params.target !== "node") return null;
if (mode === "manual" && params.target !== "node" && !params.requestedNode) return null;
const browserNodes = (await browserToolDeps.listNodes({})).filter((node) => node.connected && isBrowserNode$1(node));
if (browserNodes.length === 0) {
if (params.target === "node" || params.requestedNode) throw new Error("No connected browser-capable nodes.");
return null;
}
const requested = params.requestedNode?.trim() || policy?.node?.trim();
if (requested) {
const nodeId = resolveNodeIdFromList(browserNodes, requested, false);
const node = browserNodes.find((entry) => entry.nodeId === nodeId);
return {
nodeId,
label: node?.displayName ?? node?.remoteIp ?? nodeId
};
}
const selected = selectDefaultNodeFromList(browserNodes, {
preferLocalMac: false,
fallback: "none"
});
if (params.target === "node") {
if (selected) return {
nodeId: selected.nodeId,
label: selected.displayName ?? selected.remoteIp ?? selected.nodeId
};
throw new Error(`Multiple browser-capable nodes connected (${browserNodes.length}). Set gateway.nodes.browser.node or pass node=<id>.`);
}
if (mode === "manual") return null;
if (selected) return {
nodeId: selected.nodeId,
label: selected.displayName ?? selected.remoteIp ?? selected.nodeId
};
return null;
}
async function callBrowserProxy(params) {
const proxyTimeoutMs = typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs) ? Math.max(1, Math.floor(params.timeoutMs)) : DEFAULT_BROWSER_PROXY_TIMEOUT_MS;
const gatewayTimeoutMs = proxyTimeoutMs + BROWSER_PROXY_GATEWAY_TIMEOUT_SLACK_MS;
const parsed = unwrapBrowserProxyPayload(await browserToolDeps.callGatewayTool("node.invoke", { timeoutMs: gatewayTimeoutMs }, {
nodeId: params.nodeId,
command: "browser.proxy",
params: {
method: params.method,
path: params.path,
query: params.query,
body: params.body,
timeoutMs: proxyTimeoutMs,
profile: params.profile
},
idempotencyKey: crypto.randomUUID()
}));
if (!parsed || typeof parsed !== "object" || !("result" in parsed)) throw new Error("browser proxy failed");
return parsed;
}
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 persistProxyFiles$1(files) {
return await persistBrowserProxyFiles(files);
}
function applyProxyPaths$1(result, mapping) {
applyBrowserProxyPaths(result, mapping);
}
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.");
}
function shouldPreferHostForProfile(profileName) {
if (!profileName) return false;
const cfg = browserToolDeps.getRuntimeConfig();
const profile = resolveProfile(resolveBrowserConfig(cfg.browser, cfg), profileName);
if (!profile) return false;
return getBrowserProfileCapabilities(profile).usesChromeMcp;
}
const DEFAULT_EXISTING_SESSION_MANAGE_TIMEOUT_MS = 45e3;
const EXISTING_SESSION_MANAGE_ACTIONS = new Set([
"status",
"start",
"stop",
"profiles",
"tabs",
"open",
"focus",
"close"
]);
function usesExistingSessionManageFlow(params) {
if (!EXISTING_SESSION_MANAGE_ACTIONS.has(params.action)) return false;
const cfg = browserToolDeps.getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
const profile = resolveProfile(resolved, params.profileName ?? resolved.defaultProfile);
if (profile && getBrowserProfileCapabilities(profile).usesChromeMcp) return true;
if (params.action !== "profiles") return false;
return Object.keys(resolved.profiles).some((name) => {
const candidate = resolveProfile(resolved, name);
return candidate ? getBrowserProfileCapabilities(candidate).usesChromeMcp : false;
});
}
function readToolTimeoutMs(params) {
return readPositiveIntegerParam(params, "timeoutMs", { message: "timeoutMs must be a positive integer." });
}
/** Create the Browser tool exposed to agents. */
function createBrowserTool(opts) {
const targetDefault = opts?.sandboxBridgeUrl ? "sandbox" : "host";
const hostHint = opts?.allowHostControl === false ? "Host target blocked by policy." : "Host target allowed.";
return {
label: "Browser",
name: "browser",
description: [
"Control the browser via OpenClaw's browser control server (status/start/stop/profiles/tabs/open/snapshot/screenshot/actions).",
"Browser choice: omit profile by default for the isolated OpenClaw-managed browser (`openclaw`).",
"For the logged-in user browser, use profile=\"user\". A supported Chromium-based browser (v144+) must be running on the selected host or browser node. Use only when existing logins/cookies matter and the user is present.",
"For profile=\"user\" or other existing-session profiles, omit timeoutMs on act:type, evaluate, hover, scrollIntoView, drag, select, and fill; that driver rejects per-call timeout overrides for those actions.",
"When a node-hosted browser proxy is available, the tool may auto-route to it. Pin a node with node=<id|name> or target=\"node\".",
"When using refs from snapshot (e.g. e12), keep the same tab: prefer passing targetId from the snapshot response into subsequent actions (act/click/type/etc). For tab operations, targetId also accepts tabId handles (t1) and labels from action=tabs.",
"For multi-step browser work, login checks, stale refs, duplicate tabs, or Google Meet flows, use the bundled browser-automation skill when it is available.",
"For stable, self-resolving refs across calls, use snapshot with refs=\"aria\" (Playwright aria-ref ids). Default refs=\"role\" are role+name-based.",
"Use snapshot+act for UI automation. Avoid act:wait by default; use only in exceptional cases when no reliable UI state exists.",
`target selects browser location (sandbox|host|node). Default: ${targetDefault}.`,
hostHint
].join(" "),
parameters: BrowserToolSchema,
execute: async (_toolCallId, args) => {
const params = args;
const action = readStringParam(params, "action", { required: true });
const profile = readStringParam(params, "profile");
const requestedNode = readStringParam(params, "node");
const requestedTimeoutMs = readToolTimeoutMs(params);
let target = readStringParam(params, "target");
const configuredNode = browserToolDeps.getRuntimeConfig().gateway?.nodes?.browser?.node?.trim();
if (requestedNode && target && target !== "node") throw new Error("node is only supported with target=\"node\".");
const isUserBrowserProfile = shouldPreferHostForProfile(profile);
if (isUserBrowserProfile) {
if (target === "sandbox") throw new Error(`profile="${profile}" cannot use the sandbox browser; use target="host" or omit target.`);
}
let nodeTarget = null;
try {
nodeTarget = await resolveBrowserNodeTarget$1({
requestedNode: requestedNode ?? void 0,
target,
sandboxBridgeUrl: opts?.sandboxBridgeUrl
});
} catch (error) {
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 proxyRequest = nodeTarget ? async (optsLocal) => {
const proxy = await callBrowserProxy({
nodeId: nodeTarget.nodeId,
method: optsLocal.method,
path: optsLocal.path,
query: optsLocal.query,
body: optsLocal.body,
timeoutMs: optsLocal.timeoutMs,
profile: optsLocal.profile
});
const mapping = await persistProxyFiles$1(proxy.files);
applyProxyPaths$1(proxy.result, mapping);
return proxy.result;
} : null;
const toolTimeoutMs = requestedTimeoutMs ?? (usesExistingSessionManageFlow({
action,
profileName: profile
}) ? DEFAULT_EXISTING_SESSION_MANAGE_TIMEOUT_MS : void 0);
const touchTrackedTab = (targetId) => {
if (proxyRequest || !targetId) return;
browserToolDeps.touchSessionBrowserTab({
sessionKey: opts?.agentSessionKey,
targetId,
baseUrl,
profile
});
};
switch (action) {
case "doctor":
if (proxyRequest) return jsonResult(await proxyRequest({
method: "GET",
path: "/doctor",
profile
}));
return jsonResult(await browserToolDeps.browserDoctor(baseUrl, { profile }));
case "status":
if (proxyRequest) return jsonResult(await proxyRequest({
method: "GET",
path: "/",
profile,
timeoutMs: toolTimeoutMs
}));
return jsonResult(await browserToolDeps.browserStatus(baseUrl, {
profile,
timeoutMs: toolTimeoutMs
}));
case "start":
if (proxyRequest) {
await proxyRequest({
method: "POST",
path: "/start",
profile,
timeoutMs: toolTimeoutMs
});
return jsonResult(await proxyRequest({
method: "GET",
path: "/",
profile,
timeoutMs: toolTimeoutMs
}));
}
await browserToolDeps.browserStart(baseUrl, {
profile,
timeoutMs: toolTimeoutMs
});
return jsonResult(await browserToolDeps.browserStatus(baseUrl, {
profile,
timeoutMs: toolTimeoutMs
}));
case "stop":
if (proxyRequest) {
await proxyRequest({
method: "POST",
path: "/stop",
profile,
timeoutMs: toolTimeoutMs
});
return jsonResult(await proxyRequest({
method: "GET",
path: "/",
profile,
timeoutMs: toolTimeoutMs
}));
}
await browserToolDeps.browserStop(baseUrl, {
profile,
timeoutMs: toolTimeoutMs
});
return jsonResult(await browserToolDeps.browserStatus(baseUrl, {
profile,
timeoutMs: toolTimeoutMs
}));
case "profiles":
if (proxyRequest) return jsonResult(await proxyRequest({
method: "GET",
path: "/profiles",
timeoutMs: toolTimeoutMs
}));
return jsonResult({ profiles: await browserToolDeps.browserProfiles(baseUrl, { timeoutMs: toolTimeoutMs }) });
case "tabs": return await executeTabsAction({
baseUrl,
profile,
timeoutMs: toolTimeoutMs,
proxyRequest
});
case "open": {
const targetUrl = readTargetUrlParam(params);
const label = normalizeOptionalString(params.label);
if (proxyRequest) return jsonResult(await proxyRequest({
method: "POST",
path: "/tabs/open",
profile,
body: {
url: targetUrl,
...label ? { label } : {}
},
timeoutMs: toolTimeoutMs
}));
const opened = await browserToolDeps.browserOpenTab(baseUrl, targetUrl, {
profile,
label,
timeoutMs: toolTimeoutMs
});
browserToolDeps.trackSessionBrowserTab({
sessionKey: opts?.agentSessionKey,
targetId: opened.targetId,
baseUrl,
profile
});
return jsonResult(opened);
}
case "focus": {
const targetId = readStringParam(params, "targetId", { required: true });
if (proxyRequest) return jsonResult(await proxyRequest({
method: "POST",
path: "/tabs/focus",
profile,
body: { targetId },
timeoutMs: toolTimeoutMs
}));
await browserToolDeps.browserFocusTab(baseUrl, targetId, {
profile,
timeoutMs: toolTimeoutMs
});
touchTrackedTab(targetId);
return jsonResult({ ok: true });
}
case "close": {
const targetId = readStringParam(params, "targetId");
if (proxyRequest) return jsonResult(targetId ? await proxyRequest({
method: "DELETE",
path: `/tabs/${encodeURIComponent(targetId)}`,
profile,
timeoutMs: toolTimeoutMs
}) : await proxyRequest({
method: "POST",
path: "/act",
profile,
body: { kind: "close" },
timeoutMs: toolTimeoutMs
}));
if (targetId) {
await browserToolDeps.browserCloseTab(baseUrl, targetId, {
profile,
timeoutMs: toolTimeoutMs
});
browserToolDeps.untrackSessionBrowserTab({
sessionKey: opts?.agentSessionKey,
targetId,
baseUrl,
profile
});
} else await browserToolDeps.browserAct(baseUrl, { kind: "close" }, {
profile,
timeoutMs: toolTimeoutMs
});
return jsonResult({ ok: true });
}
case "snapshot": return await executeSnapshotAction({
input: params,
baseUrl,
profile,
proxyRequest,
onTabActivity: touchTrackedTab
});
case "screenshot": {
const targetId = readStringParam(params, "targetId");
const fullPage = Boolean(params.fullPage);
const ref = readStringParam(params, "ref");
const element = readStringParam(params, "element");
const labels = typeof params.labels === "boolean" ? params.labels : void 0;
const type = params.type === "jpeg" ? "jpeg" : "png";
const effectiveTimeoutMs = requestedTimeoutMs ?? 2e4;
const result = proxyRequest ? await proxyRequest({
method: "POST",
path: "/screenshot",
profile,
timeoutMs: effectiveTimeoutMs,
body: {
targetId,
fullPage,
ref,
element,
type,
labels,
timeoutMs: effectiveTimeoutMs
}
}) : await browserToolDeps.browserScreenshotAction(baseUrl, {
targetId,
fullPage,
ref,
element,
type,
labels,
timeoutMs: effectiveTimeoutMs,
profile
});
touchTrackedTab(readStringValue(result.targetId) ?? targetId);
const screenshotPath = result.path;
const screenshotCfg = browserToolDeps.getRuntimeConfig();
const imageSanitization = resolveRuntimeImageSanitization();
try {
const described = await describeBrowserScreenshot({
cfg: screenshotCfg,
filePath: screenshotPath,
agentDir: opts?.agentDir,
workspaceDir: opts?.workspaceDir,
activeModel: opts?.activeModel,
mediaScope: opts?.mediaScope,
imageSanitization
}, {
describeImageFile: browserToolDeps.describeImageFile,
normalizeBrowserScreenshot: browserToolDeps.normalizeBrowserScreenshot,
saveMediaBuffer: browserToolDeps.saveMediaBuffer
});
if (described) {
const headerLines = [`[analyzed by ${described.provider && described.model ? `${described.provider}/${described.model}` : "media image understanding"}]`];
const wrappedDescription = wrapExternalContent(neutralizeMediaDirectives(described.text.trim()), {
source: "browser",
includeWarning: true
});
return {
content: [{
type: "text",
text: `${headerLines.join("\n")}\n${wrappedDescription}`
}],
details: {
...result,
vision: {
provider: described.provider,
model: described.model,
decision: described.decision
}
}
};
}
} catch (err) {
const extraText = `[browser screenshot vision failed: ${neutralizeMediaDirectives(err instanceof Error ? err.message : String(err))}]`;
return await browserToolDeps.imageResultFromFile({
label: "browser:screenshot",
path: screenshotPath,
extraText,
details: result,
imageSanitization
});
}
return await browserToolDeps.imageResultFromFile({
label: "browser:screenshot",
path: screenshotPath,
details: result,
imageSanitization
});
}
case "navigate": {
const targetUrl = readTargetUrlParam(params);
const targetId = readStringParam(params, "targetId");
if (proxyRequest) return jsonResult(await proxyRequest({
method: "POST",
path: "/navigate",
profile,
body: {
url: targetUrl,
targetId
}
}));
const result = await browserToolDeps.browserNavigate(baseUrl, {
url: targetUrl,
targetId,
profile
});
touchTrackedTab(readStringValue(result.targetId) ?? targetId);
return jsonResult(result);
}
case "console": return await executeConsoleAction({
input: params,
baseUrl,
profile,
proxyRequest
});
case "pdf": {
const targetId = normalizeOptionalString(params.targetId);
const result = proxyRequest ? await proxyRequest({
method: "POST",
path: "/pdf",
profile,
body: { targetId }
}) : await browserToolDeps.browserPdfSave(baseUrl, {
targetId,
profile
});
touchTrackedTab(readStringValue(result.targetId) ?? targetId);
return {
content: [{
type: "text",
text: `FILE:${result.path}`
}],
details: result
};
}
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 normalizedPaths = resolvedResult.paths;
const ref = readStringParam(params, "ref");
const inputRef = readStringParam(params, "inputRef");
const element = readStringParam(params, "element");
const { targetId, timeoutMs } = readOptionalTargetAndTimeout(params);
if (proxyRequest) return jsonResult(await proxyRequest({
method: "POST",
path: "/hooks/file-chooser",
profile,
body: {
paths: normalizedPaths,
ref,
inputRef,
element,
targetId,
timeoutMs
}
}));
const result = await browserToolDeps.browserArmFileChooser(baseUrl, {
paths: normalizedPaths,
ref,
inputRef,
element,
targetId,
timeoutMs,
profile
});
touchTrackedTab(readStringValue(result.targetId) ?? targetId);
return jsonResult(result);
}
case "dialog": {
const accept = Boolean(params.accept);
const promptText = readStringValue(params.promptText);
const dialogId = readStringValue(params.dialogId);
const { targetId, timeoutMs } = readOptionalTargetAndTimeout(params);
if (proxyRequest) return jsonResult(await proxyRequest({
method: "POST",
path: "/hooks/dialog",
profile,
body: {
accept,
promptText,
dialogId,
targetId,
timeoutMs
}
}));
const result = await browserToolDeps.browserArmDialog(baseUrl, {
accept,
promptText,
dialogId,
targetId,
timeoutMs,
profile
});
touchTrackedTab(readStringValue(result.targetId) ?? targetId);
return jsonResult(result);
}
case "act": {
const request = readActRequestParam(params);
if (!request) throw new Error("request required");
return await executeActAction({
request,
baseUrl,
profile,
proxyRequest,
onTabActivity: touchTrackedTab
});
}
default: throw new Error(`Unknown action: ${action}`);
}
}
};
}
//#endregion
//#region extensions/browser/src/gateway/browser-request.ts
/**
* Gateway handler for browser.request, including optional node-host proxy
* dispatch and local Browser control route dispatch.
*/
function isBrowserNode(node) {
const caps = Array.isArray(node.caps) ? node.caps : [];
const commands = Array.isArray(node.commands) ? node.commands : [];
return caps.includes("browser") || commands.includes("browser.proxy");
}
function normalizeNodeKey(value) {
return normalizeLowercaseStringOrEmpty(value).replace(/[^a-z0-9]+/g, "");
}
function resolveBrowserNode(nodes, query) {
const q = normalizeOptionalString(query) ?? "";
if (!q) return null;
const qNorm = normalizeNodeKey(q);
const matches = nodes.filter((node) => {
if (node.nodeId === q) return true;
if (typeof node.remoteIp === "string" && node.remoteIp === q) return true;
const name = typeof node.displayName === "string" ? node.displayName : "";
if (name && normalizeNodeKey(name) === qNorm) return true;
if (q.length >= 6 && node.nodeId.startsWith(q)) return true;
return false;
});
if (matches.length === 1) return matches[0] ?? null;
if (matches.length === 0) return null;
throw new Error(`ambiguous node: ${q} (matches: ${matches.map((node) => node.displayName || node.remoteIp || node.nodeId).join(", ")})`);
}
function resolveBrowserNodeTarget(params) {
const policy = params.cfg.gateway?.nodes?.browser;
const mode = policy?.mode ?? "auto";
if (mode === "off") return null;
const browserNodes = params.nodes.filter((node) => isBrowserNode(node));
if (browserNodes.length === 0) {
if (normalizeOptionalString(policy?.node)) throw new Error("No connected browser-capable nodes.");
return null;
}
const requested = normalizeOptionalString(policy?.node) ?? "";
if (requested) {
const resolved = resolveBrowserNode(browserNodes, requested);
if (!resolved) throw new Error(`Configured browser node not connected: ${requested}`);
return resolved;
}
if (mode === "manual") return null;
if (browserNodes.length === 1) return browserNodes[0] ?? null;
return null;
}
async function persistProxyFiles(files) {
return await persistBrowserProxyFiles(files);
}
function applyProxyPaths(result, mapping) {
applyBrowserProxyPaths(result, mapping);
}
/** Handles one browser.request gateway call and streams a success/error response. */
async function handleBrowserGatewayRequest({ params, respond, context }) {
const typed = params;
const methodRaw = (normalizeOptionalString(typed.method) ?? "").toUpperCase();
const path = normalizeOptionalString(typed.path) ?? "";
const query = typed.query && typeof typed.query === "object" ? typed.query : void 0;
const body = typed.body;
const timeoutMs = clampTimerTimeoutMs(typed.timeoutMs);
if (!methodRaw || !path) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "method and path are required"));
return;
}
if (methodRaw !== "GET" && methodRaw !== "POST" && methodRaw !== "DELETE") {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "method must be GET, POST, or DELETE"));
return;
}
if (isPersistentBrowserProfileMutation(methodRaw, path)) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "browser.request cannot mutate persistent browser profiles"));
return;
}
const cfg = getRuntimeConfig();
let nodeTarget;
try {
nodeTarget = resolveBrowserNodeTarget({
cfg,
nodes: context.nodeRegistry.listConnected()
});
} catch (err) {
respond(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, String(err)));
return;
}
if (nodeTarget) {
const allowlist = resolveNodeCommandAllowlist(cfg, nodeTarget);
const allowed = isNodeCommandAllowed({
command: "browser.proxy",
declaredCommands: nodeTarget.commands,
allowlist
});
if (!allowed.ok) {
const platform = nodeTarget.platform ?? "unknown";
const hint = `node command not allowed: ${allowed.reason} (platform: ${platform}, command: browser.proxy)`;
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, hint, { details: {
reason: allowed.reason,
command: "browser.proxy"
} }));
return;
}
const proxyParams = {
method: methodRaw,
path,
query,
body,
timeoutMs,
profile: resolveRequestedBrowserProfile({
query,
body
})
};
const res = await context.nodeRegistry.invoke({
nodeId: nodeTarget.nodeId,
command: "browser.proxy",
params: proxyParams,
timeoutMs,
idempotencyKey: crypto.randomUUID()
});
if (!respondUnavailableOnNodeInvokeError(respond, res)) return;
const payload = res.payloadJSON ? safeParseJson(res.payloadJSON) : res.payload;
const proxy = payload && typeof payload === "object" ? payload : null;
if (!proxy || !("result" in proxy)) {
respond(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, "browser proxy failed"));
return;
}
const mapping = await persistProxyFiles(proxy.files);
applyProxyPaths(proxy.result, mapping);
respond(true, proxy.result);
return;
}
if (!await startBrowserControlServiceFromConfig()) {
respond(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, "browser control is disabled"));
return;
}
let dispatcher;
try {
dispatcher = createBrowserRouteDispatcher(createBrowserControlContext());
} catch (err) {
respond(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, String(err)));
return;
}
let result;
try {
result = timeoutMs ? await withTimeout((signal) => dispatcher.dispatch({
method: methodRaw,
path,
query,
body,
signal
}), timeoutMs, "browser request") : await dispatcher.dispatch({
method: methodRaw,
path,
query,
body
});
} catch (err) {
respond(false, void 0, errorShape(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})`;
respond(false, void 0, errorShape(result.status >= 500 ? ErrorCodes.UNAVAILABLE : ErrorCodes.INVALID_REQUEST, message, { details: result.body }));
return;
}
respond(true, result.body);
}
/** Gateway request handler map contributed by the Browser plugin. */
const browserHandlers = { "browser.request": handleBrowserGatewayRequest };
//#endregion
//#region extensions/browser/src/plugin-service.ts
/**
* Browser plugin service factory that lazily starts the control server.
*/
const EAGER_BROWSER_CONTROL_SERVICE_ENV = "OPENCLAW_EAGER_BROWSER_CONTROL_SERVER";
const UNSAFE_BROWSER_CONTROL_OVERRIDE_SPECIFIER = /^(?:data|http|https|node):/i;
function isTruthyEnvValue(value) {
return /^(?:1|true|yes|on)$/iu.test(value?.trim() ?? "");
}
function validateBrowserControlOverrideSpecifier(specifier) {
const trimmed = specifier.trim();
if (UNSAFE_BROWSER_CONTROL_OVERRIDE_SPECIFIER.test(trimmed)) throw new Error(`Refusing unsafe browser control override specifier: ${trimmed}`);
return trimmed;
}
/** Creates the Browser plugin service registered by the plugin entrypoint. */
function createBrowserPluginService() {
let handle = null;
return {
id: "browser-control",
start: async () => {
if (!isTruthyEnvValue(process.env[EAGER_BROWSER_CONTROL_SERVICE_ENV])) return;
if (handle) return;
handle = await startLazyPluginServiceModule({
skipEnvVar: "OPENCLAW_SKIP_BROWSER_CONTROL_SERVER",
overrideEnvVar: "OPENCLAW_BROWSER_CONTROL_MODULE",
validateOverrideSpecifier: validateBrowserControlOverrideSpecifier,
loadDefaultModule: async () => await import("./server-BUcgFvOO.js"),
startExportNames: ["startBrowserControlServiceFromConfig", "startBrowserControlServerFromConfig"],
stopExportNames: ["stopBrowserControlService", "stopBrowserControlServer"]
});
},
stop: async () => {
const current = handle;
handle = null;
if (current) {
await current.stop().catch(() => {});
return;
}
const { stopBrowserControlService } = await import("./control-service-C-wICWzm.js");
await stopBrowserControlService().catch(() => {});
}
};
}
//#endregion
export { createBrowserTool as i, browserHandlers as n, handleBrowserGatewayRequest as r, createBrowserPluginService as t };