openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
392 lines (391 loc) • 21.4 kB
JavaScript
import { l as normalizeOptionalString } from "./string-coerce-1VuiLaaR.mjs";
import "./string-coerce-runtime-50qKeWeL.mjs";
import { a as optionalNonNegativeIntegerSchema, c as optionalStringEnum, i as optionalFiniteNumberSchema, l as stringEnum, o as optionalPositiveIntegerSchema } from "./typebox-X-jU33BZ.mjs";
import "./channel-actions-DF1kQl0D.mjs";
import { n as ACT_MAX_VIEWPORT_DIMENSION } from "./act-policy-PVRotc-k.mjs";
import { i as resolveBrowserConfig, s as resolveProfile } from "./config-DQZ6ubGi.mjs";
import { t as getBrowserProfileCapabilities } from "./profile-capabilities-DHBwe9FJ.mjs";
import { Type } from "typebox";
//#region extensions/browser/src/browser-node-commands.ts
const BROWSER_PROXY_COMMAND = "browser.proxy";
const BROWSER_PROXY_UPLOAD_COMMAND = "browser.proxy.upload.v1";
function browserProxyUploadUnavailableMessage(pendingDeclaredCommands) {
return pendingDeclaredCommands?.includes("browser.proxy.upload.v1") ? "browser node remote upload transfer is pending approval; approve the node's pending command update before retrying" : "browser node does not support remote upload transfer; update the node or approve its pending command update before retrying";
}
//#endregion
//#region extensions/browser/src/browser-tool-binding.ts
/** Validate the plugin-owned run binding before any browser route is resolved. */
function parseBrowserTabToolBinding(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) return {
ok: false,
error: "browser tool binding must be an object"
};
const record = value;
const target = record.target === "host" || record.target === "node" ? record.target : void 0;
const node = normalizeOptionalString(record.node);
const profile = normalizeOptionalString(record.profile);
const targetId = normalizeOptionalString(record.targetId);
if (record.kind !== "tab") return {
ok: false,
error: "browser tool binding kind must be \"tab\""
};
if (!Number.isSafeInteger(record.tabId) || Number(record.tabId) < 0) return {
ok: false,
error: "browser tool binding tabId must be a non-negative integer"
};
if (!target || !profile || !targetId || target === "node" && !node) return {
ok: false,
error: "browser tool binding requires target, profile, and targetId"
};
if (target === "host" && node) return {
ok: false,
error: "browser host binding cannot include node"
};
return {
ok: true,
binding: {
kind: "tab",
tabId: Number(record.tabId),
target,
...node ? { node } : {},
profile,
targetId
}
};
}
const BROWSER_TAB_BOUND_ACTIONS = [
"act",
"close",
"console",
"requests",
"errors",
"text",
"emulate",
"dialog",
"download",
"focus",
"navigate",
"pdf",
"screenshot",
"snapshot",
"tabs",
"upload",
"waitfordownload"
];
function bindTargetId(record, targetId) {
const requestedTargetId = normalizeOptionalString(record.targetId);
if (requestedTargetId && requestedTargetId !== targetId) throw new Error("browser action cannot override its run-bound tab target");
const actions = Array.isArray(record.actions) ? record.actions.map((action) => action && typeof action === "object" && !Array.isArray(action) ? bindTargetId(action, targetId) : action) : record.actions;
return {
...record,
targetId,
...actions ? { actions } : {}
};
}
/** Pin model-supplied browser arguments to the trusted tab route for this run. */
function applyBrowserTabToolBinding(input, binding) {
const requestedTarget = normalizeOptionalString(input.target);
const requestedNode = normalizeOptionalString(input.node);
const requestedProfile = normalizeOptionalString(input.profile);
if (requestedTarget && requestedTarget !== binding.target) throw new Error("browser action cannot override its run-bound target");
if (requestedNode && requestedNode !== binding.node) throw new Error("browser action cannot override its run-bound node");
if (requestedProfile && requestedProfile !== binding.profile) throw new Error("browser action cannot override its run-bound profile");
const bound = bindTargetId(input, binding.targetId);
const request = bound.request && typeof bound.request === "object" && !Array.isArray(bound.request) ? bindTargetId(bound.request, binding.targetId) : bound.request;
return {
...bound,
target: binding.target,
...binding.node ? { node: binding.node } : {},
profile: binding.profile,
...request ? { request } : {}
};
}
//#endregion
//#region extensions/browser/src/browser-tool.schema.ts
const BROWSER_ACT_KINDS = [
"batch",
"click",
"clickCoords",
"type",
"press",
"hover",
"scrollIntoView",
"drag",
"select",
"fill",
"resize",
"wait",
"evaluate",
"close"
];
const BROWSER_TOOL_ACTIONS = [
"doctor",
"status",
"start",
"stop",
"profiles",
"importprofile",
"tabs",
"open",
"focus",
"close",
"snapshot",
"screenshot",
"navigate",
"console",
"requests",
"errors",
"text",
"emulate",
"pdf",
"download",
"waitfordownload",
"upload",
"dialog",
"act"
];
const BROWSER_TARGETS = [
"sandbox",
"host",
"node"
];
const BROWSER_SNAPSHOT_FORMATS = ["aria", "ai"];
const BROWSER_SNAPSHOT_MODES = ["efficient"];
const BROWSER_SNAPSHOT_REFS = ["role", "aria"];
const BROWSER_IMAGE_TYPES = ["png", "jpeg"];
const TAB_REFERENCE_DESCRIPTION = "Prefer suggestedTargetId/tabId/label; raw CDP targetId.";
function resolveBrowserToolCapabilities(params) {
const evaluateEnabled = params?.evaluateEnabled !== false;
const profileCapabilities = params?.profileCapabilities;
return {
actions: (params?.tabBound ? BROWSER_TAB_BOUND_ACTIONS : BROWSER_TOOL_ACTIONS).filter((action) => (profileCapabilities?.supportsPdf !== false || action !== "pdf") && (profileCapabilities?.supportsRequests !== false || action !== "requests") && (profileCapabilities?.supportsErrors !== false || action !== "errors") && (profileCapabilities?.supportsPageText !== false || action !== "text") && (profileCapabilities?.supportsEmulation !== false || action !== "emulate") && (profileCapabilities?.supportsScreenshots !== false || action !== "screenshot") && (profileCapabilities?.supportsUploads !== false || action !== "upload") && (profileCapabilities?.supportsDialogs !== false || action !== "dialog") && (profileCapabilities?.supportsConsole !== false || action !== "console") && (profileCapabilities?.supportsDownloads !== false || action !== "download" && action !== "waitfordownload")),
actKinds: BROWSER_ACT_KINDS.filter((kind) => (evaluateEnabled || kind !== "evaluate") && (profileCapabilities?.supportsBatchActions !== false || kind !== "batch") && (profileCapabilities?.supportsVisualActions !== false || kind !== "clickCoords" && kind !== "drag" && kind !== "resize" && kind !== "hover" && kind !== "scrollIntoView")),
tabBound: params?.tabBound === true,
supportsNativeSnapshots: profileCapabilities?.supportsNativeSnapshots !== false
};
}
function createBrowserActProperties(capabilities) {
const supportsBatch = capabilities.actKinds.includes("batch");
return {
targetId: Type.Optional(Type.String({ description: TAB_REFERENCE_DESCRIPTION })),
ref: Type.Optional(Type.String({ description: "snapshot ref." })),
actions: Type.Optional(Type.Array(Type.Object({}, { additionalProperties: true }), supportsBatch ? { description: "batch actions." } : {})),
stopOnError: Type.Optional(Type.Boolean(supportsBatch ? { description: "Stop on error; default true." } : {})),
doubleClick: Type.Optional(Type.Boolean({ description: "Double-click/clickCoords." })),
button: Type.Optional(Type.String()),
modifiers: Type.Optional(Type.Array(Type.String())),
...capabilities.actKinds.includes("clickCoords") ? {
x: optionalFiniteNumberSchema(),
y: optionalFiniteNumberSchema()
} : {},
text: Type.Optional(Type.String()),
submit: Type.Optional(Type.Boolean()),
slowly: Type.Optional(Type.Boolean()),
key: Type.Optional(Type.String({ description: "Escape, Enter, Control+Shift+T; aliases Esc, Return, Del, Ctrl, Cmd." })),
delayMs: optionalNonNegativeIntegerSchema(),
...capabilities.actKinds.includes("drag") ? {
startRef: Type.Optional(Type.String()),
endRef: Type.Optional(Type.String())
} : {},
values: Type.Optional(Type.Array(Type.String())),
fields: Type.Optional(Type.Array(Type.Object({}, { additionalProperties: true }))),
...capabilities.actKinds.includes("resize") ? {
width: optionalPositiveIntegerSchema({ maximum: ACT_MAX_VIEWPORT_DIMENSION }),
height: optionalPositiveIntegerSchema({ maximum: ACT_MAX_VIEWPORT_DIMENSION })
} : {},
timeMs: optionalNonNegativeIntegerSchema(),
...capabilities.supportsNativeSnapshots !== false ? { selector: Type.Optional(Type.String()) } : {},
url: Type.Optional(Type.String()),
loadState: Type.Optional(Type.String()),
textGone: Type.Optional(Type.String()),
timeoutMs: optionalPositiveIntegerSchema(),
...capabilities.actKinds.includes("evaluate") ? { fn: Type.Optional(Type.String()) } : {}
};
}
function createBrowserToolSchema(capabilities) {
const actProperties = createBrowserActProperties(capabilities);
const actKindDescription = capabilities.actKinds.includes("batch") ? "batch uses actions." : "Act kind.";
const BrowserActSchema = Type.Object({
kind: stringEnum(capabilities.actKinds, { description: actKindDescription }),
...actProperties
}, { description: "act" });
return Type.Object({
action: stringEnum(capabilities.actions),
target: optionalStringEnum(BROWSER_TARGETS),
...!capabilities.tabBound ? { dashboard: Type.Optional(Type.String({
pattern: "^[a-z0-9][a-z0-9._-]{0,63}$",
description: "browser:dashboard widget name."
})) } : {},
node: Type.Optional(Type.String()),
profile: Type.Optional(Type.String({ description: capabilities.tabBound ? "Run-bound browser profile." : "default if omitted." })),
browser: Type.Optional(Type.String()),
systemProfile: Type.Optional(Type.String()),
into: Type.Optional(Type.String()),
domains: Type.Optional(Type.Array(Type.String())),
targetUrl: Type.Optional(Type.String()),
label: Type.Optional(Type.String()),
limit: optionalPositiveIntegerSchema(),
maxChars: optionalNonNegativeIntegerSchema(),
mode: optionalStringEnum(BROWSER_SNAPSHOT_MODES),
snapshotFormat: optionalStringEnum(capabilities.supportsNativeSnapshots === false ? ["ai"] : BROWSER_SNAPSHOT_FORMATS),
refs: optionalStringEnum(capabilities.supportsNativeSnapshots === false ? ["aria"] : BROWSER_SNAPSHOT_REFS),
interactive: Type.Optional(Type.Boolean()),
compact: Type.Optional(Type.Boolean()),
depth: optionalNonNegativeIntegerSchema(),
...capabilities.supportsNativeSnapshots !== false ? { frame: Type.Optional(Type.String()) } : {},
...capabilities.actions.includes("screenshot") ? {
labels: Type.Optional(Type.Boolean({ description: "Label snapshot/screenshot." })),
fullPage: Type.Optional(Type.Boolean()),
element: Type.Optional(Type.String()),
type: optionalStringEnum(BROWSER_IMAGE_TYPES)
} : {},
urls: Type.Optional(Type.Boolean()),
...capabilities.actions.some((action) => [
"screenshot",
"pdf",
"download",
"waitfordownload"
].includes(action)) ? { path: Type.Optional(Type.String()) } : {},
level: Type.Optional(Type.String()),
filter: Type.Optional(Type.String()),
clear: Type.Optional(Type.Boolean()),
query: Type.Optional(Type.String()),
...capabilities.actions.includes("emulate") ? {
device: Type.Optional(Type.String()),
colorScheme: optionalStringEnum([
"dark",
"light",
"no-preference",
"none"
]),
timezoneId: Type.Optional(Type.String()),
locale: Type.Optional(Type.String())
} : {},
...capabilities.actions.includes("upload") ? {
paths: Type.Optional(Type.Array(Type.String())),
inputRef: Type.Optional(Type.String())
} : {},
...capabilities.actions.includes("dialog") ? {
dialogId: Type.Optional(Type.String()),
accept: Type.Optional(Type.Boolean()),
promptText: Type.Optional(Type.String())
} : {},
kind: Type.Optional(stringEnum(capabilities.actKinds, { description: actKindDescription })),
...actProperties,
request: Type.Optional(BrowserActSchema)
});
}
const BrowserSnapshotStatsSchema = Type.Object({
lines: Type.Number(),
chars: Type.Number(),
refs: Type.Number(),
interactive: Type.Number()
}, { additionalProperties: false });
const BrowserBatchAbortSchema = Type.Object({
reason: stringEnum(["navigation", "closed"]),
afterAction: Type.Number(),
url: Type.String(),
skipped: Type.Number()
}, { additionalProperties: false });
const BrowserToolOutputSchema = Type.Object({
ok: Type.Optional(Type.Boolean()),
targetId: Type.Optional(Type.String()),
url: Type.Optional(Type.String()),
format: Type.Optional(stringEnum(BROWSER_SNAPSHOT_FORMATS)),
snapshot: Type.Optional(Type.String()),
refs: Type.Optional(Type.Union([Type.Number(), Type.Record(Type.String(), Type.Unknown())])),
stats: Type.Optional(BrowserSnapshotStatsSchema),
truncated: Type.Optional(Type.Boolean()),
newElements: Type.Optional(Type.Number()),
tabs: Type.Optional(Type.Array(Type.Object({
suggestedTargetId: Type.Optional(Type.String()),
tabId: Type.Optional(Type.String()),
webExtensionTabId: Type.Optional(Type.Integer({ minimum: 0 })),
label: Type.Optional(Type.String()),
targetId: Type.Optional(Type.String()),
title: Type.Optional(Type.String()),
url: Type.Optional(Type.String()),
urlUnavailableReason: optionalStringEnum(["navigation_blocked", "navigation_check_failed"]),
type: Type.Optional(Type.String())
}, { additionalProperties: true }))),
tabCount: Type.Optional(Type.Number()),
results: Type.Optional(Type.Array(Type.Object({
ok: Type.Boolean(),
error: Type.Optional(Type.String()),
navigated: Type.Optional(Type.Literal(true)),
url: Type.Optional(Type.String())
}, { additionalProperties: false }))),
aborted: Type.Optional(BrowserBatchAbortSchema),
pageState: Type.Optional(Type.Object({}, {
additionalProperties: true,
description: "Inline snapshot details attached when the action changed the page document."
})),
enabled: Type.Optional(Type.Boolean()),
running: Type.Optional(Type.Boolean()),
profile: Type.Optional(Type.String()),
driver: Type.Optional(Type.String()),
transport: Type.Optional(Type.String()),
pid: Type.Optional(Type.Union([Type.Number(), Type.Null()])),
cdpPort: Type.Optional(Type.Union([Type.Number(), Type.Null()])),
cdpUrl: Type.Optional(Type.Union([Type.String(), Type.Null()]))
}, { additionalProperties: true });
//#endregion
//#region extensions/browser/src/browser-tool-description.ts
/** Lazy registration and execution expose exactly the same configured tool. */
function createBrowserToolDefinition(opts, getConfig) {
const parsed = opts?.runToolBinding === void 0 ? void 0 : parseBrowserTabToolBinding(opts.runToolBinding);
if (parsed && !parsed.ok) throw new Error(`invalid browser run binding: ${parsed.error}`);
const binding = parsed?.binding;
const capabilities = opts?.toolCapabilities ?? (() => {
const config = getConfig();
const profile = binding?.target === "host" ? resolveProfile(resolveBrowserConfig(config?.browser, config), binding.profile) : void 0;
return resolveBrowserToolCapabilities({
tabBound: Boolean(binding),
evaluateEnabled: config?.browser?.evaluateEnabled !== false,
...profile ? { profileCapabilities: getBrowserProfileCapabilities(profile) } : {}
});
})();
return {
binding,
capabilities,
metadata: {
label: "Browser",
name: "browser",
resultContentSource: "network",
description: describeBrowserTool({
targetDefault: opts?.sandboxBridgeUrl ? "sandbox" : "host",
hostHint: opts?.allowHostControl === false ? "Host target blocked by policy." : "Host target allowed.",
capabilities
}),
parameters: createBrowserToolSchema(capabilities),
outputSchema: BrowserToolOutputSchema
}
};
}
function describeBrowserTool(opts) {
const actions = new Set(opts.capabilities.actions);
const evaluateEnabled = opts.capabilities.actKinds.includes("evaluate");
return [
`Control the browser via OpenClaw's browser control server. Available actions: ${opts.capabilities.actions.join(", ")}.`,
...actions.has("profiles") ? ["Browser choice: omit profile to use the configured default (normally the isolated OpenClaw-managed `openclaw` browser).", "When existing logins/cookies matter, use action=profiles to inspect available profiles, then select the appropriate profile by name. Do not assume a profile name."] : [],
...actions.has("importprofile") ? ["Use action=importprofile on macOS to copy cookies from an authorized Chrome-family system profile into a fresh managed profile; this may show a Keychain consent prompt."] : [],
`For Chrome MCP existing-session profiles, omit timeoutMs on act:type, hover, scrollIntoView, drag, select, and fill; that driver rejects per-call timeout overrides for those actions.${evaluateEnabled ? " act:evaluate supports timeoutMs." : ""}`,
...!opts.capabilities.tabBound ? ["Prefer the host browser; auto-route to a connected browser node only when the host has no usable browser capability. Select another location with target=\"node\" or node=<id|name>; configured node pins also take precedence."] : [],
"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.",
...!opts.capabilities.tabBound ? ["Only create a Browser dashboard when the user asks for a dashboard. Opening the browser sidebar or side panel does not require a widget. For a requested agent-controllable HTTP(S) dashboard, first call tool dashboard with action=\"widget_put\", pluginKind=\"browser:dashboard\", name=<stable widget name>, props={url}, and size=\"full\". Next call tool browser with action=\"open\", dashboard=<that widget name>, and no targetUrl. Then call tool dashboard with action=\"set_presentation\", presentation=\"expanded\". The dashboard tool owns widget authoring and presentation; the browser tool owns page interaction. Use dashboard=<widget name> and omit targetId/profile/route overrides. Administrator dashboards use managed-profile state. Non-admin selectors use an empty isolated context in the configured default local managed profile; supported actions are tabs, focus, navigate, snapshot, screenshot, act, open and close. Session mode is unavailable when a sandbox is required or model selection is locked. Agent and viewer must use the same mode to share page state. Hiding a dashboard preserves its page; resetting a session closes its isolated context. General browser actions retain their configured host/profile access. Call browser with action=\"close\" and dashboard to request stop, or action=\"open\" and dashboard to resume its saved URL. A session:website widget is a lightweight iframe and cannot be controlled through this selector."] : [],
"For stable, self-resolving refs across calls, use snapshot with refs=\"aria\" (Playwright aria-ref ids). Default refs=\"role\" are role+name-based.",
"Repeated compatible snapshots with stable document identity mark newly appeared ref-bearing elements with [new].",
`navigate returns the loaded page's compact snapshot inline (efficient interactive tier; use action=snapshot for a full snapshot); do not call snapshot after navigate.${opts.capabilities.actKinds.includes("batch") ? " Batch act results that report a cross-document navigation also include fresh page state;" : ""} After a single act that triggers navigation, snapshot before using refs.`,
"Use snapshot+act for UI automation. Avoid act:wait by default; use only in exceptional cases when no reliable UI state exists.",
actions.has("text") ? "For page prose, use action=text with optional selector and maxChars; it reads the first selector match, else article, main, or body. Use efficient snapshots for controls; they omit most prose." : `For page text, use snapshot${evaluateEnabled ? " or a bounded act:evaluate" : ""}; efficient snapshots omit most prose.`,
"Use snapshot query to keep lines matching all whitespace-separated tokens, case-insensitively; matching lines retain element refs.",
...actions.has("requests") ? ["Use requests for the recent network log; filter matches URL/type, limit defaults to 50, and clear=true clears the collected log after reading."] : [],
...actions.has("errors") ? ["Use errors for page errors; limit defaults to 50, clear=true clears after reading."] : [],
...actions.has("emulate") ? ["Use emulate with device, colorScheme, timezoneId, or locale; at least one setting is required."] : [],
...actions.has("upload") ? ["For file chooser uploads, pass the trigger ref with paths in the same upload call when available; use paths-only arming only when a later trigger is intentional. Use inputRef or element to set a file input directly."] : [],
...!opts.capabilities.tabBound ? [`target selects browser location (sandbox|host|node). Default: ${opts.targetDefault}.`, opts.hostHint] : []
].join(" ");
}
//#endregion
export { browserProxyUploadUnavailableMessage as a, BROWSER_PROXY_UPLOAD_COMMAND as i, applyBrowserTabToolBinding as n, BROWSER_PROXY_COMMAND as r, createBrowserToolDefinition as t };