@gguf/claw
Version:
Multi-channel AI gateway with extensible messaging integrations
1,447 lines (1,430 loc) • 268 kB
JavaScript
import { i as resolveGatewayPort, t as STATE_DIR, u as resolveRequiredHomeDir } from "./paths-Bp5uKvNR.js";
import { d as resolveAgentIdFromSessionKey, i as buildAgentMainSessionKey, l as normalizeAgentId, n as DEFAULT_AGENT_ID, u as normalizeMainKey } from "./session-key-CgcjHuX_.js";
import { A as normalizeE164, P as resolveUserPath, V as truncateUtf16Safe, lt as resolvePreferredOpenClawTmpDir, t as CHANNEL_IDS } from "./registry-dD2_jBuv.js";
import { E as runExec, _ as DEFAULT_SOUL_FILENAME, b as ensureAgentWorkspace, f as DEFAULT_AGENTS_FILENAME, g as DEFAULT_IDENTITY_FILENAME, h as DEFAULT_HEARTBEAT_FILENAME, m as DEFAULT_BOOTSTRAP_FILENAME, n as resolveAgentConfig, p as DEFAULT_AGENT_WORKSPACE_DIR, u as resolveSessionAgentId, v as DEFAULT_TOOLS_FILENAME, y as DEFAULT_USER_FILENAME } from "./agent-scope-DiocriBl.js";
import { r as defaultRuntime, t as createSubsystemLogger } from "./subsystem-CGx2ESmP.js";
import { J as isPidAlive, q as resolveProcessScopedMap } from "./model-selection-Xwi9H_jm.js";
import { n as parseBooleanValue, r as formatCliCommand } from "./env-MtDjQbRJ.js";
import { t as normalizeChatType } from "./chat-type-DFDuk3FY.js";
import { r as normalizeChannelId } from "./plugins-C2vFjSxX.js";
import { a as buildImageResizeSideGrid, i as IMAGE_REDUCE_QUALITY_STEPS, n as openFileWithinRoot, s as getImageMetadata, t as SafeOpenError, u as resizeToJpeg } from "./fs-safe-GrTh3Ydq.js";
import { o as listDeliverableMessageChannels, s as normalizeMessageChannel } from "./message-channel-BxsLQyfw.js";
import { _ as parseDurationMs, a as writeConfigFile, n as loadConfig, s as parseByteSize, t as createConfigIO } from "./config-42hNXHap.js";
import { d as wrapOwnerOnlyToolExecution, f as sanitizeContentBlocksImages } from "./common-BLbt_nyn.js";
import { A as DEFAULT_BROWSER_EVALUATE_ENABLED, D as DEFAULT_AI_SNAPSHOT_EFFICIENT_MAX_CHARS, E as DEFAULT_AI_SNAPSHOT_EFFICIENT_DEPTH, M as DEFAULT_OPENCLAW_BROWSER_ENABLED, N as DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME, O as DEFAULT_AI_SNAPSHOT_MAX_CHARS, S as stopChromeExtensionRelayServer, _ as fetchJson, a as resolveOpenClawUserDataDir, c as captureScreenshot, d as normalizeCdpWsUrl, f as snapshotAria, g as appendCdpPath, h as withBrowserNavigationPolicy, i as launchOpenClawChrome, j as DEFAULT_OPENCLAW_BROWSER_COLOR, k as DEFAULT_BROWSER_DEFAULT_PROFILE_NAME, l as createTargetViaCdp, m as assertBrowserNavigationAllowed, n as isChromeCdpReady, o as stopOpenClawChrome, p as InvalidBrowserNavigationUrlError, r as isChromeReachable, s as resolveBrowserExecutableForPlatform, v as fetchOk, w as isLoopbackHost, x as ensureChromeExtensionRelayServer } from "./chrome-DQPy_ANn.js";
import { a as syncSkillsToWorkspace, l as resolveSandboxInputPath, m as sanitizeEnvVars, u as resolveSandboxPath } from "./skills-Bu-sOzsy.js";
import { n as formatErrorMessage, t as extractErrorCode } from "./errors-BxR4oB-s.js";
import { t as SsrFBlockedError } from "./ssrf-DH-Ltk62.js";
import { r as saveMediaBuffer, t as ensureMediaDir } from "./store-D4zsSWrO.js";
import { d as getChannelDock, o as normalizeThinkLevel, v as normalizeHyphenSlug } from "./thinking-CzOYfSuG.js";
import { n as resolveConversationLabel } from "./conversation-label-BqCuLhlF.js";
import { a as resolveSessionTranscriptPathInDir, i as resolveSessionTranscriptPath, n as resolveSessionFilePath, s as resolveStorePath, t as resolveDefaultSessionStorePath } from "./paths-COn9vdws.js";
import { t as emitSessionTranscriptUpdate } from "./transcript-events-Cj85Mq0h.js";
import path, { posix } from "node:path";
import fs, { existsSync, realpathSync } from "node:fs";
import os from "node:os";
import fs$1 from "node:fs/promises";
import { spawn } from "node:child_process";
import crypto, { createHash, timingSafeEqual } from "node:crypto";
import { CURRENT_SESSION_VERSION, SessionManager } from "@mariozechner/pi-coding-agent";
import express from "express";
//#region src/agents/pi-embedded-helpers/bootstrap.ts
function isBase64Signature(value) {
const trimmed = value.trim();
if (!trimmed) return false;
const compact = trimmed.replace(/\s+/g, "");
if (!/^[A-Za-z0-9+/=_-]+$/.test(compact)) return false;
const isUrl = compact.includes("-") || compact.includes("_");
try {
const buf = Buffer.from(compact, isUrl ? "base64url" : "base64");
if (buf.length === 0) return false;
const encoded = buf.toString(isUrl ? "base64url" : "base64");
const normalize = (input) => input.replace(/=+$/g, "");
return normalize(encoded) === normalize(compact);
} catch {
return false;
}
}
/**
* Strips Claude-style thought_signature fields from content blocks.
*
* Gemini expects thought signatures as base64-encoded bytes, but Claude stores message ids
* like "msg_abc123...". We only strip "msg_*" to preserve any provider-valid signatures.
*/
function stripThoughtSignatures(content, options) {
if (!Array.isArray(content)) return content;
const allowBase64Only = options?.allowBase64Only ?? false;
const includeCamelCase = options?.includeCamelCase ?? false;
const shouldStripSignature = (value) => {
if (!allowBase64Only) return typeof value === "string" && value.startsWith("msg_");
return typeof value !== "string" || !isBase64Signature(value);
};
return content.map((block) => {
if (!block || typeof block !== "object") return block;
const rec = block;
const stripSnake = shouldStripSignature(rec.thought_signature);
const stripCamel = includeCamelCase ? shouldStripSignature(rec.thoughtSignature) : false;
if (!stripSnake && !stripCamel) return block;
const next = { ...rec };
if (stripSnake) delete next.thought_signature;
if (stripCamel) delete next.thoughtSignature;
return next;
});
}
const DEFAULT_BOOTSTRAP_MAX_CHARS = 2e4;
const DEFAULT_BOOTSTRAP_TOTAL_MAX_CHARS = 15e4;
const MIN_BOOTSTRAP_FILE_BUDGET_CHARS = 64;
const BOOTSTRAP_HEAD_RATIO = .7;
const BOOTSTRAP_TAIL_RATIO = .2;
function resolveBootstrapMaxChars(cfg) {
const raw = cfg?.agents?.defaults?.bootstrapMaxChars;
if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) return Math.floor(raw);
return DEFAULT_BOOTSTRAP_MAX_CHARS;
}
function resolveBootstrapTotalMaxChars(cfg) {
const raw = cfg?.agents?.defaults?.bootstrapTotalMaxChars;
if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) return Math.floor(raw);
return DEFAULT_BOOTSTRAP_TOTAL_MAX_CHARS;
}
function trimBootstrapContent(content, fileName, maxChars) {
const trimmed = content.trimEnd();
if (trimmed.length <= maxChars) return {
content: trimmed,
truncated: false,
maxChars,
originalLength: trimmed.length
};
const headChars = Math.floor(maxChars * BOOTSTRAP_HEAD_RATIO);
const tailChars = Math.floor(maxChars * BOOTSTRAP_TAIL_RATIO);
const head = trimmed.slice(0, headChars);
const tail = trimmed.slice(-tailChars);
return {
content: [
head,
[
"",
`[...truncated, read ${fileName} for full content...]`,
`…(truncated ${fileName}: kept ${headChars}+${tailChars} chars of ${trimmed.length})…`,
""
].join("\n"),
tail
].join("\n"),
truncated: true,
maxChars,
originalLength: trimmed.length
};
}
function clampToBudget(content, budget) {
if (budget <= 0) return "";
if (content.length <= budget) return content;
if (budget <= 3) return truncateUtf16Safe(content, budget);
return `${truncateUtf16Safe(content, budget - 1)}…`;
}
async function ensureSessionHeader$1(params) {
const file = params.sessionFile;
try {
await fs$1.stat(file);
return;
} catch {}
await fs$1.mkdir(path.dirname(file), { recursive: true });
const entry = {
type: "session",
version: 2,
id: params.sessionId,
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
cwd: params.cwd
};
await fs$1.writeFile(file, `${JSON.stringify(entry)}\n`, "utf-8");
}
function buildBootstrapContextFiles(files, opts) {
const maxChars = opts?.maxChars ?? DEFAULT_BOOTSTRAP_MAX_CHARS;
let remainingTotalChars = Math.max(1, Math.floor(opts?.totalMaxChars ?? Math.max(maxChars, DEFAULT_BOOTSTRAP_TOTAL_MAX_CHARS)));
const result = [];
for (const file of files) {
if (remainingTotalChars <= 0) break;
if (file.missing) {
const cappedMissingText = clampToBudget(`[MISSING] Expected at: ${file.path}`, remainingTotalChars);
if (!cappedMissingText) break;
remainingTotalChars = Math.max(0, remainingTotalChars - cappedMissingText.length);
result.push({
path: file.path,
content: cappedMissingText
});
continue;
}
if (remainingTotalChars < MIN_BOOTSTRAP_FILE_BUDGET_CHARS) {
opts?.warn?.(`remaining bootstrap budget is ${remainingTotalChars} chars (<${MIN_BOOTSTRAP_FILE_BUDGET_CHARS}); skipping additional bootstrap files`);
break;
}
const fileMaxChars = Math.max(1, Math.min(maxChars, remainingTotalChars));
const trimmed = trimBootstrapContent(file.content ?? "", file.name, fileMaxChars);
const contentWithinBudget = clampToBudget(trimmed.content, remainingTotalChars);
if (!contentWithinBudget) continue;
if (trimmed.truncated || contentWithinBudget.length < trimmed.content.length) opts?.warn?.(`workspace bootstrap file ${file.name} is ${trimmed.originalLength} chars (limit ${trimmed.maxChars}); truncating in injected context`);
remainingTotalChars = Math.max(0, remainingTotalChars - contentWithinBudget.length);
result.push({
path: file.path,
content: contentWithinBudget
});
}
return result;
}
function sanitizeGoogleTurnOrdering(messages) {
const GOOGLE_TURN_ORDER_BOOTSTRAP_TEXT = "(session bootstrap)";
const first = messages[0];
const role = first?.role;
const content = first?.content;
if (role === "user" && typeof content === "string" && content.trim() === GOOGLE_TURN_ORDER_BOOTSTRAP_TEXT) return messages;
if (role !== "assistant") return messages;
return [{
role: "user",
content: GOOGLE_TURN_ORDER_BOOTSTRAP_TEXT,
timestamp: Date.now()
}, ...messages];
}
//#endregion
//#region src/agents/sandbox/constants.ts
const DEFAULT_SANDBOX_WORKSPACE_ROOT = path.join(STATE_DIR, "sandboxes");
const DEFAULT_SANDBOX_IMAGE = "openclaw-sandbox:bookworm-slim";
const DEFAULT_SANDBOX_CONTAINER_PREFIX = "openclaw-sbx-";
const DEFAULT_SANDBOX_WORKDIR = "/workspace";
const DEFAULT_SANDBOX_IDLE_HOURS = 24;
const DEFAULT_SANDBOX_MAX_AGE_DAYS = 7;
const DEFAULT_TOOL_ALLOW = [
"exec",
"process",
"read",
"write",
"edit",
"apply_patch",
"image",
"sessions_list",
"sessions_history",
"sessions_send",
"sessions_spawn",
"subagents",
"session_status"
];
const DEFAULT_TOOL_DENY = [
"browser",
"canvas",
"nodes",
"cron",
"gateway",
...CHANNEL_IDS
];
const DEFAULT_SANDBOX_BROWSER_IMAGE = "openclaw-sandbox-browser:bookworm-slim";
const DEFAULT_SANDBOX_BROWSER_PREFIX = "openclaw-sbx-browser-";
const DEFAULT_SANDBOX_BROWSER_CDP_PORT = 9222;
const DEFAULT_SANDBOX_BROWSER_VNC_PORT = 5900;
const DEFAULT_SANDBOX_BROWSER_NOVNC_PORT = 6080;
const DEFAULT_SANDBOX_BROWSER_AUTOSTART_TIMEOUT_MS = 12e3;
const SANDBOX_AGENT_WORKSPACE_MOUNT = "/agent";
const SANDBOX_STATE_DIR = path.join(STATE_DIR, "sandbox");
const SANDBOX_REGISTRY_PATH = path.join(SANDBOX_STATE_DIR, "containers.json");
const SANDBOX_BROWSER_REGISTRY_PATH = path.join(SANDBOX_STATE_DIR, "browsers.json");
//#endregion
//#region src/agents/glob-pattern.ts
function escapeRegex(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function compileGlobPattern(params) {
const normalized = params.normalize(params.raw);
if (!normalized) return {
kind: "exact",
value: ""
};
if (normalized === "*") return { kind: "all" };
if (!normalized.includes("*")) return {
kind: "exact",
value: normalized
};
return {
kind: "regex",
value: new RegExp(`^${escapeRegex(normalized).replaceAll("\\*", ".*")}$`)
};
}
function compileGlobPatterns(params) {
if (!Array.isArray(params.raw)) return [];
return params.raw.map((raw) => compileGlobPattern({
raw,
normalize: params.normalize
})).filter((pattern) => pattern.kind !== "exact" || pattern.value);
}
function matchesAnyGlobPattern(value, patterns) {
for (const pattern of patterns) {
if (pattern.kind === "all") return true;
if (pattern.kind === "exact" && value === pattern.value) return true;
if (pattern.kind === "regex" && pattern.value.test(value)) return true;
}
return false;
}
//#endregion
//#region src/agents/tool-policy.ts
const TOOL_NAME_ALIASES = {
bash: "exec",
"apply-patch": "apply_patch"
};
const TOOL_GROUPS = {
"group:memory": ["memory_search", "memory_get"],
"group:web": ["web_search", "web_fetch"],
"group:fs": [
"read",
"write",
"edit",
"apply_patch"
],
"group:runtime": ["exec", "process"],
"group:sessions": [
"sessions_list",
"sessions_history",
"sessions_send",
"sessions_spawn",
"subagents",
"session_status"
],
"group:ui": ["browser", "canvas"],
"group:automation": ["cron", "gateway"],
"group:messaging": ["message"],
"group:nodes": ["nodes"],
"group:openclaw": [
"browser",
"canvas",
"nodes",
"cron",
"message",
"gateway",
"agents_list",
"sessions_list",
"sessions_history",
"sessions_send",
"sessions_spawn",
"subagents",
"session_status",
"memory_search",
"memory_get",
"web_search",
"web_fetch",
"image"
]
};
const OWNER_ONLY_TOOL_NAME_FALLBACKS = new Set([
"whatsapp_login",
"cron",
"gateway"
]);
const TOOL_PROFILES = {
minimal: { allow: ["session_status"] },
coding: { allow: [
"group:fs",
"group:runtime",
"group:sessions",
"group:memory",
"image"
] },
messaging: { allow: [
"group:messaging",
"sessions_list",
"sessions_history",
"sessions_send",
"session_status"
] },
full: {}
};
function normalizeToolName(name) {
const normalized = name.trim().toLowerCase();
return TOOL_NAME_ALIASES[normalized] ?? normalized;
}
function isOwnerOnlyToolName(name) {
return OWNER_ONLY_TOOL_NAME_FALLBACKS.has(normalizeToolName(name));
}
function isOwnerOnlyTool(tool) {
return tool.ownerOnly === true || isOwnerOnlyToolName(tool.name);
}
function applyOwnerOnlyToolPolicy(tools, senderIsOwner) {
const withGuard = tools.map((tool) => {
if (!isOwnerOnlyTool(tool)) return tool;
return wrapOwnerOnlyToolExecution(tool, senderIsOwner);
});
if (senderIsOwner) return withGuard;
return withGuard.filter((tool) => !isOwnerOnlyTool(tool));
}
function normalizeToolList(list) {
if (!list) return [];
return list.map(normalizeToolName).filter(Boolean);
}
function expandToolGroups(list) {
const normalized = normalizeToolList(list);
const expanded = [];
for (const value of normalized) {
const group = TOOL_GROUPS[value];
if (group) {
expanded.push(...group);
continue;
}
expanded.push(value);
}
return Array.from(new Set(expanded));
}
function collectExplicitAllowlist(policies) {
const entries = [];
for (const policy of policies) {
if (!policy?.allow) continue;
for (const value of policy.allow) {
if (typeof value !== "string") continue;
const trimmed = value.trim();
if (trimmed) entries.push(trimmed);
}
}
return entries;
}
function buildPluginToolGroups(params) {
const all = [];
const byPlugin = /* @__PURE__ */ new Map();
for (const tool of params.tools) {
const meta = params.toolMeta(tool);
if (!meta) continue;
const name = normalizeToolName(tool.name);
all.push(name);
const pluginId = meta.pluginId.toLowerCase();
const list = byPlugin.get(pluginId) ?? [];
list.push(name);
byPlugin.set(pluginId, list);
}
return {
all,
byPlugin
};
}
function expandPluginGroups(list, groups) {
if (!list || list.length === 0) return list;
const expanded = [];
for (const entry of list) {
const normalized = normalizeToolName(entry);
if (normalized === "group:plugins") {
if (groups.all.length > 0) expanded.push(...groups.all);
else expanded.push(normalized);
continue;
}
const tools = groups.byPlugin.get(normalized);
if (tools && tools.length > 0) {
expanded.push(...tools);
continue;
}
expanded.push(normalized);
}
return Array.from(new Set(expanded));
}
function expandPolicyWithPluginGroups(policy, groups) {
if (!policy) return;
return {
allow: expandPluginGroups(policy.allow, groups),
deny: expandPluginGroups(policy.deny, groups)
};
}
function stripPluginOnlyAllowlist(policy, groups, coreTools) {
if (!policy?.allow || policy.allow.length === 0) return {
policy,
unknownAllowlist: [],
strippedAllowlist: false
};
const normalized = normalizeToolList(policy.allow);
if (normalized.length === 0) return {
policy,
unknownAllowlist: [],
strippedAllowlist: false
};
const pluginIds = new Set(groups.byPlugin.keys());
const pluginTools = new Set(groups.all);
const unknownAllowlist = [];
let hasCoreEntry = false;
for (const entry of normalized) {
if (entry === "*") {
hasCoreEntry = true;
continue;
}
const isPluginEntry = entry === "group:plugins" || pluginIds.has(entry) || pluginTools.has(entry);
const isCoreEntry = expandToolGroups([entry]).some((tool) => coreTools.has(tool));
if (isCoreEntry) hasCoreEntry = true;
if (!isCoreEntry && !isPluginEntry) unknownAllowlist.push(entry);
}
const strippedAllowlist = !hasCoreEntry;
if (strippedAllowlist) {}
return {
policy: strippedAllowlist ? {
...policy,
allow: void 0
} : policy,
unknownAllowlist: Array.from(new Set(unknownAllowlist)),
strippedAllowlist
};
}
function resolveToolProfilePolicy(profile) {
if (!profile) return;
const resolved = TOOL_PROFILES[profile];
if (!resolved) return;
if (!resolved.allow && !resolved.deny) return;
return {
allow: resolved.allow ? [...resolved.allow] : void 0,
deny: resolved.deny ? [...resolved.deny] : void 0
};
}
function mergeAlsoAllowPolicy(policy, alsoAllow) {
if (!policy?.allow || !Array.isArray(alsoAllow) || alsoAllow.length === 0) return policy;
return {
...policy,
allow: Array.from(new Set([...policy.allow, ...alsoAllow]))
};
}
//#endregion
//#region src/agents/sandbox/tool-policy.ts
function normalizeGlob(value) {
return value.trim().toLowerCase();
}
function isToolAllowed(policy, name) {
const normalized = normalizeGlob(name);
if (matchesAnyGlobPattern(normalized, compileGlobPatterns({
raw: expandToolGroups(policy.deny ?? []),
normalize: normalizeGlob
}))) return false;
const allow = compileGlobPatterns({
raw: expandToolGroups(policy.allow ?? []),
normalize: normalizeGlob
});
if (allow.length === 0) return true;
return matchesAnyGlobPattern(normalized, allow);
}
function resolveSandboxToolPolicyForAgent(cfg, agentId) {
const agentConfig = cfg && agentId ? resolveAgentConfig(cfg, agentId) : void 0;
const agentAllow = agentConfig?.tools?.sandbox?.tools?.allow;
const agentDeny = agentConfig?.tools?.sandbox?.tools?.deny;
const globalAllow = cfg?.tools?.sandbox?.tools?.allow;
const globalDeny = cfg?.tools?.sandbox?.tools?.deny;
const allowSource = Array.isArray(agentAllow) ? {
source: "agent",
key: "agents.list[].tools.sandbox.tools.allow"
} : Array.isArray(globalAllow) ? {
source: "global",
key: "tools.sandbox.tools.allow"
} : {
source: "default",
key: "tools.sandbox.tools.allow"
};
const denySource = Array.isArray(agentDeny) ? {
source: "agent",
key: "agents.list[].tools.sandbox.tools.deny"
} : Array.isArray(globalDeny) ? {
source: "global",
key: "tools.sandbox.tools.deny"
} : {
source: "default",
key: "tools.sandbox.tools.deny"
};
const deny = Array.isArray(agentDeny) ? agentDeny : Array.isArray(globalDeny) ? globalDeny : [...DEFAULT_TOOL_DENY];
const allow = Array.isArray(agentAllow) ? agentAllow : Array.isArray(globalAllow) ? globalAllow : [...DEFAULT_TOOL_ALLOW];
const expandedDeny = expandToolGroups(deny);
let expandedAllow = expandToolGroups(allow);
if (expandedAllow.length > 0 && !expandedDeny.map((v) => v.toLowerCase()).includes("image") && !expandedAllow.map((v) => v.toLowerCase()).includes("image")) expandedAllow = [...expandedAllow, "image"];
return {
allow: expandedAllow,
deny: expandedDeny,
sources: {
allow: allowSource,
deny: denySource
}
};
}
//#endregion
//#region src/agents/sandbox/config.ts
function resolveSandboxBrowserDockerCreateConfig(params) {
const base = {
...params.docker,
network: "bridge",
image: params.browser.image
};
return params.browser.binds !== void 0 ? {
...base,
binds: params.browser.binds
} : base;
}
function resolveSandboxScope(params) {
if (params.scope) return params.scope;
if (typeof params.perSession === "boolean") return params.perSession ? "session" : "shared";
return "agent";
}
function resolveSandboxDockerConfig(params) {
const agentDocker = params.scope === "shared" ? void 0 : params.agentDocker;
const globalDocker = params.globalDocker;
const env = agentDocker?.env ? {
...globalDocker?.env ?? { LANG: "C.UTF-8" },
...agentDocker.env
} : globalDocker?.env ?? { LANG: "C.UTF-8" };
const ulimits = agentDocker?.ulimits ? {
...globalDocker?.ulimits,
...agentDocker.ulimits
} : globalDocker?.ulimits;
const binds = [...globalDocker?.binds ?? [], ...agentDocker?.binds ?? []];
return {
image: agentDocker?.image ?? globalDocker?.image ?? DEFAULT_SANDBOX_IMAGE,
containerPrefix: agentDocker?.containerPrefix ?? globalDocker?.containerPrefix ?? DEFAULT_SANDBOX_CONTAINER_PREFIX,
workdir: agentDocker?.workdir ?? globalDocker?.workdir ?? DEFAULT_SANDBOX_WORKDIR,
readOnlyRoot: agentDocker?.readOnlyRoot ?? globalDocker?.readOnlyRoot ?? true,
tmpfs: agentDocker?.tmpfs ?? globalDocker?.tmpfs ?? [
"/tmp",
"/var/tmp",
"/run"
],
network: agentDocker?.network ?? globalDocker?.network ?? "none",
user: agentDocker?.user ?? globalDocker?.user,
capDrop: agentDocker?.capDrop ?? globalDocker?.capDrop ?? ["ALL"],
env,
setupCommand: agentDocker?.setupCommand ?? globalDocker?.setupCommand,
pidsLimit: agentDocker?.pidsLimit ?? globalDocker?.pidsLimit,
memory: agentDocker?.memory ?? globalDocker?.memory,
memorySwap: agentDocker?.memorySwap ?? globalDocker?.memorySwap,
cpus: agentDocker?.cpus ?? globalDocker?.cpus,
ulimits,
seccompProfile: agentDocker?.seccompProfile ?? globalDocker?.seccompProfile,
apparmorProfile: agentDocker?.apparmorProfile ?? globalDocker?.apparmorProfile,
dns: agentDocker?.dns ?? globalDocker?.dns,
extraHosts: agentDocker?.extraHosts ?? globalDocker?.extraHosts,
binds: binds.length ? binds : void 0
};
}
function resolveSandboxBrowserConfig(params) {
const agentBrowser = params.scope === "shared" ? void 0 : params.agentBrowser;
const globalBrowser = params.globalBrowser;
const binds = [...globalBrowser?.binds ?? [], ...agentBrowser?.binds ?? []];
const bindsConfigured = globalBrowser?.binds !== void 0 || agentBrowser?.binds !== void 0;
return {
enabled: agentBrowser?.enabled ?? globalBrowser?.enabled ?? false,
image: agentBrowser?.image ?? globalBrowser?.image ?? DEFAULT_SANDBOX_BROWSER_IMAGE,
containerPrefix: agentBrowser?.containerPrefix ?? globalBrowser?.containerPrefix ?? DEFAULT_SANDBOX_BROWSER_PREFIX,
cdpPort: agentBrowser?.cdpPort ?? globalBrowser?.cdpPort ?? DEFAULT_SANDBOX_BROWSER_CDP_PORT,
vncPort: agentBrowser?.vncPort ?? globalBrowser?.vncPort ?? DEFAULT_SANDBOX_BROWSER_VNC_PORT,
noVncPort: agentBrowser?.noVncPort ?? globalBrowser?.noVncPort ?? DEFAULT_SANDBOX_BROWSER_NOVNC_PORT,
headless: agentBrowser?.headless ?? globalBrowser?.headless ?? false,
enableNoVnc: agentBrowser?.enableNoVnc ?? globalBrowser?.enableNoVnc ?? true,
allowHostControl: agentBrowser?.allowHostControl ?? globalBrowser?.allowHostControl ?? false,
autoStart: agentBrowser?.autoStart ?? globalBrowser?.autoStart ?? true,
autoStartTimeoutMs: agentBrowser?.autoStartTimeoutMs ?? globalBrowser?.autoStartTimeoutMs ?? DEFAULT_SANDBOX_BROWSER_AUTOSTART_TIMEOUT_MS,
binds: bindsConfigured ? binds : void 0
};
}
function resolveSandboxPruneConfig(params) {
const agentPrune = params.scope === "shared" ? void 0 : params.agentPrune;
const globalPrune = params.globalPrune;
return {
idleHours: agentPrune?.idleHours ?? globalPrune?.idleHours ?? DEFAULT_SANDBOX_IDLE_HOURS,
maxAgeDays: agentPrune?.maxAgeDays ?? globalPrune?.maxAgeDays ?? DEFAULT_SANDBOX_MAX_AGE_DAYS
};
}
function resolveSandboxConfigForAgent(cfg, agentId) {
const agent = cfg?.agents?.defaults?.sandbox;
let agentSandbox;
const agentConfig = cfg && agentId ? resolveAgentConfig(cfg, agentId) : void 0;
if (agentConfig?.sandbox) agentSandbox = agentConfig.sandbox;
const scope = resolveSandboxScope({
scope: agentSandbox?.scope ?? agent?.scope,
perSession: agentSandbox?.perSession ?? agent?.perSession
});
const toolPolicy = resolveSandboxToolPolicyForAgent(cfg, agentId);
return {
mode: agentSandbox?.mode ?? agent?.mode ?? "off",
scope,
workspaceAccess: agentSandbox?.workspaceAccess ?? agent?.workspaceAccess ?? "none",
workspaceRoot: agentSandbox?.workspaceRoot ?? agent?.workspaceRoot ?? DEFAULT_SANDBOX_WORKSPACE_ROOT,
docker: resolveSandboxDockerConfig({
scope,
globalDocker: agent?.docker,
agentDocker: agentSandbox?.docker
}),
browser: resolveSandboxBrowserConfig({
scope,
globalBrowser: agent?.browser,
agentBrowser: agentSandbox?.browser
}),
tools: {
allow: toolPolicy.allow,
deny: toolPolicy.deny
},
prune: resolveSandboxPruneConfig({
scope,
globalPrune: agent?.prune,
agentPrune: agentSandbox?.prune
})
};
}
//#endregion
//#region src/security/secret-equal.ts
function safeEqualSecret(provided, expected) {
if (typeof provided !== "string" || typeof expected !== "string") return false;
const hash = (s) => createHash("sha256").update(s).digest();
return timingSafeEqual(hash(provided), hash(expected));
}
//#endregion
//#region src/gateway/auth.ts
function resolveGatewayAuth(params) {
const baseAuthConfig = params.authConfig ?? {};
const authOverride = params.authOverride ?? void 0;
const authConfig = { ...baseAuthConfig };
if (authOverride) {
if (authOverride.mode !== void 0) authConfig.mode = authOverride.mode;
if (authOverride.token !== void 0) authConfig.token = authOverride.token;
if (authOverride.password !== void 0) authConfig.password = authOverride.password;
if (authOverride.allowTailscale !== void 0) authConfig.allowTailscale = authOverride.allowTailscale;
if (authOverride.rateLimit !== void 0) authConfig.rateLimit = authOverride.rateLimit;
if (authOverride.trustedProxy !== void 0) authConfig.trustedProxy = authOverride.trustedProxy;
}
const env = params.env ?? process.env;
const token = authConfig.token ?? env.OPENCLAW_GATEWAY_TOKEN ?? void 0;
const password = authConfig.password ?? env.OPENCLAW_GATEWAY_PASSWORD ?? void 0;
const trustedProxy = authConfig.trustedProxy;
let mode;
let modeSource;
if (authOverride?.mode !== void 0) {
mode = authOverride.mode;
modeSource = "override";
} else if (authConfig.mode) {
mode = authConfig.mode;
modeSource = "config";
} else if (password) {
mode = "password";
modeSource = "password";
} else if (token) {
mode = "token";
modeSource = "token";
} else {
mode = "token";
modeSource = "default";
}
const allowTailscale = authConfig.allowTailscale ?? (params.tailscaleMode === "serve" && mode !== "password" && mode !== "trusted-proxy");
return {
mode,
modeSource,
token,
password,
allowTailscale,
trustedProxy
};
}
//#endregion
//#region src/gateway/startup-auth.ts
function mergeGatewayTailscaleConfig(base, override) {
const merged = { ...base };
if (!override) return merged;
if (override.mode !== void 0) merged.mode = override.mode;
if (override.resetOnExit !== void 0) merged.resetOnExit = override.resetOnExit;
return merged;
}
function resolveGatewayAuthFromConfig(params) {
const tailscaleConfig = mergeGatewayTailscaleConfig(params.cfg.gateway?.tailscale, params.tailscaleOverride);
return resolveGatewayAuth({
authConfig: params.cfg.gateway?.auth,
authOverride: params.authOverride,
env: params.env,
tailscaleMode: tailscaleConfig.mode ?? "off"
});
}
function shouldPersistGeneratedToken(params) {
if (!params.persistRequested) return false;
if (params.resolvedAuth.modeSource === "override") return false;
return true;
}
async function ensureGatewayStartupAuth(params) {
const env = params.env ?? process.env;
const persistRequested = params.persist === true;
const resolved = resolveGatewayAuthFromConfig({
cfg: params.cfg,
env,
authOverride: params.authOverride,
tailscaleOverride: params.tailscaleOverride
});
if (resolved.mode !== "token" || (resolved.token?.trim().length ?? 0) > 0) {
assertHooksTokenSeparateFromGatewayAuth({
cfg: params.cfg,
auth: resolved
});
return {
cfg: params.cfg,
auth: resolved,
persistedGeneratedToken: false
};
}
const generatedToken = crypto.randomBytes(24).toString("hex");
const nextCfg = {
...params.cfg,
gateway: {
...params.cfg.gateway,
auth: {
...params.cfg.gateway?.auth,
mode: "token",
token: generatedToken
}
}
};
const persist = shouldPersistGeneratedToken({
persistRequested,
resolvedAuth: resolved
});
if (persist) await writeConfigFile(nextCfg);
const nextAuth = resolveGatewayAuthFromConfig({
cfg: nextCfg,
env,
authOverride: params.authOverride,
tailscaleOverride: params.tailscaleOverride
});
assertHooksTokenSeparateFromGatewayAuth({
cfg: nextCfg,
auth: nextAuth
});
return {
cfg: nextCfg,
auth: nextAuth,
generatedToken,
persistedGeneratedToken: persist
};
}
function assertHooksTokenSeparateFromGatewayAuth(params) {
if (params.cfg.hooks?.enabled !== true) return;
const hooksToken = typeof params.cfg.hooks.token === "string" ? params.cfg.hooks.token.trim() : "";
if (!hooksToken) return;
const gatewayToken = params.auth.mode === "token" && typeof params.auth.token === "string" ? params.auth.token.trim() : "";
if (!gatewayToken) return;
if (hooksToken !== gatewayToken) return;
throw new Error("Invalid config: hooks.token must not match gateway auth token. Set a distinct hooks.token for hook ingress.");
}
//#endregion
//#region src/browser/control-auth.ts
function resolveBrowserControlAuth(cfg, env = process.env) {
const auth = resolveGatewayAuth({
authConfig: cfg?.gateway?.auth,
env,
tailscaleMode: cfg?.gateway?.tailscale?.mode
});
const token = typeof auth.token === "string" ? auth.token.trim() : "";
const password = typeof auth.password === "string" ? auth.password.trim() : "";
return {
token: token || void 0,
password: password || void 0
};
}
function shouldAutoGenerateBrowserAuth(env) {
if ((env.NODE_ENV ?? "").trim().toLowerCase() === "test") return false;
const vitest = (env.VITEST ?? "").trim().toLowerCase();
if (vitest && vitest !== "0" && vitest !== "false" && vitest !== "off") return false;
return true;
}
async function ensureBrowserControlAuth(params) {
const env = params.env ?? process.env;
const auth = resolveBrowserControlAuth(params.cfg, env);
if (auth.token || auth.password) return { auth };
if (!shouldAutoGenerateBrowserAuth(env)) return { auth };
if (params.cfg.gateway?.auth?.mode === "password") return { auth };
if (params.cfg.gateway?.auth?.mode === "none") return { auth };
if (params.cfg.gateway?.auth?.mode === "trusted-proxy") return { auth };
const latestCfg = loadConfig();
const latestAuth = resolveBrowserControlAuth(latestCfg, env);
if (latestAuth.token || latestAuth.password) return { auth: latestAuth };
if (latestCfg.gateway?.auth?.mode === "password") return { auth: latestAuth };
if (latestCfg.gateway?.auth?.mode === "none") return { auth: latestAuth };
if (latestCfg.gateway?.auth?.mode === "trusted-proxy") return { auth: latestAuth };
const ensured = await ensureGatewayStartupAuth({
cfg: latestCfg,
env,
persist: true
});
return {
auth: resolveBrowserControlAuth(ensured.cfg, env),
generatedToken: ensured.generatedToken
};
}
//#endregion
//#region src/browser/bridge-auth-registry.ts
const authByPort = /* @__PURE__ */ new Map();
function setBridgeAuthForPort(port, auth) {
if (!Number.isFinite(port) || port <= 0) return;
const token = typeof auth.token === "string" ? auth.token.trim() : "";
const password = typeof auth.password === "string" ? auth.password.trim() : "";
authByPort.set(port, {
token: token || void 0,
password: password || void 0
});
}
function getBridgeAuthForPort(port) {
if (!Number.isFinite(port) || port <= 0) return;
return authByPort.get(port);
}
function deleteBridgeAuthForPort(port) {
if (!Number.isFinite(port) || port <= 0) return;
authByPort.delete(port);
}
//#endregion
//#region src/browser/routes/agent.act.shared.ts
const ACT_KINDS = [
"click",
"close",
"drag",
"evaluate",
"fill",
"hover",
"scrollIntoView",
"press",
"resize",
"select",
"type",
"wait"
];
function isActKind(value) {
if (typeof value !== "string") return false;
return ACT_KINDS.includes(value);
}
const ALLOWED_CLICK_MODIFIERS = new Set([
"Alt",
"Control",
"ControlOrMeta",
"Meta",
"Shift"
]);
function parseClickButton(raw) {
if (raw === "left" || raw === "right" || raw === "middle") return raw;
}
function parseClickModifiers(raw) {
if (raw.filter((m) => !ALLOWED_CLICK_MODIFIERS.has(m)).length) return { error: "modifiers must be Alt|Control|ControlOrMeta|Meta|Shift" };
return { modifiers: raw.length ? raw : void 0 };
}
//#endregion
//#region src/browser/pw-ai-module.ts
let pwAiModuleSoft = null;
let pwAiModuleStrict = null;
function isModuleNotFoundError(err) {
if (extractErrorCode(err) === "ERR_MODULE_NOT_FOUND") return true;
const msg = formatErrorMessage(err);
return msg.includes("Cannot find module") || msg.includes("Cannot find package") || msg.includes("Failed to resolve import") || msg.includes("Failed to resolve entry for package") || msg.includes("Failed to load url");
}
async function loadPwAiModule(mode) {
try {
return await import("./pw-ai-CgMauE2z.js");
} catch (err) {
if (mode === "soft") return null;
if (isModuleNotFoundError(err)) return null;
throw err;
}
}
async function getPwAiModule$1(opts) {
if ((opts?.mode ?? "soft") === "soft") {
if (!pwAiModuleSoft) pwAiModuleSoft = loadPwAiModule("soft");
return await pwAiModuleSoft;
}
if (!pwAiModuleStrict) pwAiModuleStrict = loadPwAiModule("strict");
return await pwAiModuleStrict;
}
//#endregion
//#region src/browser/routes/utils.ts
/**
* Extract profile name from query string or body and get profile context.
* Query string takes precedence over body for consistency with GET routes.
*/
function getProfileContext(req, ctx) {
let profileName;
if (typeof req.query.profile === "string") profileName = req.query.profile.trim() || void 0;
if (!profileName && req.body && typeof req.body === "object") {
const body = req.body;
if (typeof body.profile === "string") profileName = body.profile.trim() || void 0;
}
try {
return ctx.forProfile(profileName);
} catch (err) {
return {
error: String(err),
status: 404
};
}
}
function jsonError(res, status, message) {
res.status(status).json({ error: message });
}
function toStringOrEmpty(value) {
if (typeof value === "string") return value.trim();
if (typeof value === "number" || typeof value === "boolean") return String(value).trim();
return "";
}
function toNumber(value) {
if (typeof value === "number" && Number.isFinite(value)) return value;
if (typeof value === "string" && value.trim()) {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : void 0;
}
}
function toBoolean(value) {
return parseBooleanValue(value, {
truthy: [
"true",
"1",
"yes"
],
falsy: [
"false",
"0",
"no"
]
});
}
function toStringArray(value) {
if (!Array.isArray(value)) return;
const strings = value.map((v) => toStringOrEmpty(v)).filter(Boolean);
return strings.length ? strings : void 0;
}
//#endregion
//#region src/browser/routes/agent.shared.ts
const SELECTOR_UNSUPPORTED_MESSAGE = [
"Error: 'selector' is not supported. Use 'ref' from snapshot instead.",
"",
"Example workflow:",
"1. snapshot action to get page state with refs",
"2. act with ref: \"e123\" to interact with element",
"",
"This is more reliable for modern SPAs."
].join("\n");
function readBody(req) {
const body = req.body;
if (!body || typeof body !== "object" || Array.isArray(body)) return {};
return body;
}
function resolveTargetIdFromBody(body) {
return (typeof body.targetId === "string" ? body.targetId.trim() : "") || void 0;
}
function resolveTargetIdFromQuery(query) {
return (typeof query.targetId === "string" ? query.targetId.trim() : "") || void 0;
}
function handleRouteError(ctx, res, err) {
const mapped = ctx.mapTabError(err);
if (mapped) return jsonError(res, mapped.status, mapped.message);
jsonError(res, 500, String(err));
}
function resolveProfileContext(req, res, ctx) {
const profileCtx = getProfileContext(req, ctx);
if ("error" in profileCtx) {
jsonError(res, profileCtx.status, profileCtx.error);
return null;
}
return profileCtx;
}
async function getPwAiModule() {
return await getPwAiModule$1({ mode: "soft" });
}
async function requirePwAi(res, feature) {
const mod = await getPwAiModule();
if (mod) return mod;
jsonError(res, 501, [
`Playwright is not available in this gateway build; '${feature}' is unsupported.`,
"Install the full Playwright package (not playwright-core) and restart the gateway, or reinstall with browser support.",
"Docs: /tools/browser#playwright-requirement"
].join("\n"));
return null;
}
async function withRouteTabContext(params) {
const profileCtx = resolveProfileContext(params.req, params.res, params.ctx);
if (!profileCtx) return;
try {
const tab = await profileCtx.ensureTabAvailable(params.targetId);
return await params.run({
profileCtx,
tab,
cdpUrl: profileCtx.profile.cdpUrl
});
} catch (err) {
handleRouteError(params.ctx, params.res, err);
return;
}
}
async function withPlaywrightRouteContext(params) {
return await withRouteTabContext({
req: params.req,
res: params.res,
ctx: params.ctx,
targetId: params.targetId,
run: async ({ profileCtx, tab, cdpUrl }) => {
const pw = await requirePwAi(params.res, params.feature);
if (!pw) return;
return await params.run({
profileCtx,
tab,
cdpUrl,
pw
});
}
});
}
//#endregion
//#region src/browser/paths.ts
const DEFAULT_BROWSER_TMP_DIR = resolvePreferredOpenClawTmpDir();
const DEFAULT_TRACE_DIR = DEFAULT_BROWSER_TMP_DIR;
const DEFAULT_DOWNLOAD_DIR = path.join(DEFAULT_BROWSER_TMP_DIR, "downloads");
const DEFAULT_UPLOAD_DIR = path.join(DEFAULT_BROWSER_TMP_DIR, "uploads");
function resolvePathWithinRoot(params) {
const root = path.resolve(params.rootDir);
const raw = params.requestedPath.trim();
if (!raw) {
if (!params.defaultFileName) return {
ok: false,
error: "path is required"
};
return {
ok: true,
path: path.join(root, params.defaultFileName)
};
}
const resolved = path.resolve(root, raw);
const rel = path.relative(root, resolved);
if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) return {
ok: false,
error: `Invalid path: must stay within ${params.scopeLabel}`
};
return {
ok: true,
path: resolved
};
}
async function resolveExistingPathsWithinRoot(params) {
const resolvedPaths = [];
for (const raw of params.requestedPaths) {
const pathResult = resolvePathWithinRoot({
rootDir: params.rootDir,
requestedPath: raw,
scopeLabel: params.scopeLabel
});
if (!pathResult.ok) return {
ok: false,
error: pathResult.error
};
const rootDir = path.resolve(params.rootDir);
const relativePath = path.relative(rootDir, pathResult.path);
let opened;
try {
opened = await openFileWithinRoot({
rootDir,
relativePath
});
resolvedPaths.push(opened.realPath);
} catch (err) {
if (err instanceof SafeOpenError && err.code === "not-found") {
resolvedPaths.push(pathResult.path);
continue;
}
return {
ok: false,
error: `Invalid path: must stay within ${params.scopeLabel} and be a regular non-symlink file`
};
} finally {
await opened?.handle.close().catch(() => {});
}
}
return {
ok: true,
paths: resolvedPaths
};
}
//#endregion
//#region src/browser/routes/agent.act.ts
function resolveDownloadPathOrRespond(res, requestedPath) {
const downloadPathResult = resolvePathWithinRoot({
rootDir: DEFAULT_DOWNLOAD_DIR,
requestedPath,
scopeLabel: "downloads directory"
});
if (!downloadPathResult.ok) {
res.status(400).json({ error: downloadPathResult.error });
return null;
}
return downloadPathResult.path;
}
function buildDownloadRequestBase(cdpUrl, targetId, timeoutMs) {
return {
cdpUrl,
targetId,
timeoutMs: timeoutMs ?? void 0
};
}
function respondWithDownloadResult(res, targetId, result) {
res.json({
ok: true,
targetId,
download: result
});
}
function registerBrowserAgentActRoutes(app, ctx) {
app.post("/act", async (req, res) => {
const body = readBody(req);
const kindRaw = toStringOrEmpty(body.kind);
if (!isActKind(kindRaw)) return jsonError(res, 400, "kind is required");
const kind = kindRaw;
const targetId = resolveTargetIdFromBody(body);
if (Object.hasOwn(body, "selector") && kind !== "wait") return jsonError(res, 400, SELECTOR_UNSUPPORTED_MESSAGE);
await withPlaywrightRouteContext({
req,
res,
ctx,
targetId,
feature: `act:${kind}`,
run: async ({ cdpUrl, tab, pw }) => {
const evaluateEnabled = ctx.state().resolved.evaluateEnabled;
switch (kind) {
case "click": {
const ref = toStringOrEmpty(body.ref);
if (!ref) return jsonError(res, 400, "ref is required");
const doubleClick = toBoolean(body.doubleClick) ?? false;
const timeoutMs = toNumber(body.timeoutMs);
const buttonRaw = toStringOrEmpty(body.button) || "";
const button = buttonRaw ? parseClickButton(buttonRaw) : void 0;
if (buttonRaw && !button) return jsonError(res, 400, "button must be left|right|middle");
const parsedModifiers = parseClickModifiers(toStringArray(body.modifiers) ?? []);
if (parsedModifiers.error) return jsonError(res, 400, parsedModifiers.error);
const modifiers = parsedModifiers.modifiers;
const clickRequest = {
cdpUrl,
targetId: tab.targetId,
ref,
doubleClick
};
if (button) clickRequest.button = button;
if (modifiers) clickRequest.modifiers = modifiers;
if (timeoutMs) clickRequest.timeoutMs = timeoutMs;
await pw.clickViaPlaywright(clickRequest);
return res.json({
ok: true,
targetId: tab.targetId,
url: tab.url
});
}
case "type": {
const ref = toStringOrEmpty(body.ref);
if (!ref) return jsonError(res, 400, "ref is required");
if (typeof body.text !== "string") return jsonError(res, 400, "text is required");
const text = body.text;
const submit = toBoolean(body.submit) ?? false;
const slowly = toBoolean(body.slowly) ?? false;
const timeoutMs = toNumber(body.timeoutMs);
const typeRequest = {
cdpUrl,
targetId: tab.targetId,
ref,
text,
submit,
slowly
};
if (timeoutMs) typeRequest.timeoutMs = timeoutMs;
await pw.typeViaPlaywright(typeRequest);
return res.json({
ok: true,
targetId: tab.targetId
});
}
case "press": {
const key = toStringOrEmpty(body.key);
if (!key) return jsonError(res, 400, "key is required");
const delayMs = toNumber(body.delayMs);
await pw.pressKeyViaPlaywright({
cdpUrl,
targetId: tab.targetId,
key,
delayMs: delayMs ?? void 0
});
return res.json({
ok: true,
targetId: tab.targetId
});
}
case "hover": {
const ref = toStringOrEmpty(body.ref);
if (!ref) return jsonError(res, 400, "ref is required");
const timeoutMs = toNumber(body.timeoutMs);
await pw.hoverViaPlaywright({
cdpUrl,
targetId: tab.targetId,
ref,
timeoutMs: timeoutMs ?? void 0
});
return res.json({
ok: true,
targetId: tab.targetId
});
}
case "scrollIntoView": {
const ref = toStringOrEmpty(body.ref);
if (!ref) return jsonError(res, 400, "ref is required");
const timeoutMs = toNumber(body.timeoutMs);
const scrollRequest = {
cdpUrl,
targetId: tab.targetId,
ref
};
if (timeoutMs) scrollRequest.timeoutMs = timeoutMs;
await pw.scrollIntoViewViaPlaywright(scrollRequest);
return res.json({
ok: true,
targetId: tab.targetId
});
}
case "drag": {
const startRef = toStringOrEmpty(body.startRef);
const endRef = toStringOrEmpty(body.endRef);
if (!startRef || !endRef) return jsonError(res, 400, "startRef and endRef are required");
const timeoutMs = toNumber(body.timeoutMs);
await pw.dragViaPlaywright({
cdpUrl,
targetId: tab.targetId,
startRef,
endRef,
timeoutMs: timeoutMs ?? void 0
});
return res.json({
ok: true,
targetId: tab.targetId
});
}
case "select": {
const ref = toStringOrEmpty(body.ref);
const values = toStringArray(body.values);
if (!ref || !values?.length) return jsonError(res, 400, "ref and values are required");
const timeoutMs = toNumber(body.timeoutMs);
await pw.selectOptionViaPlaywright({
cdpUrl,
targetId: tab.targetId,
ref,
values,
timeoutMs: timeoutMs ?? void 0
});
return res.json({
ok: true,
targetId: tab.targetId
});
}
case "fill": {
const fields = (Array.isArray(body.fields) ? body.fields : []).map((field) => {
if (!field || typeof field !== "object") return null;
const rec = field;
const ref = toStringOrEmpty(rec.ref);
const type = toStringOrEmpty(rec.type);
if (!ref || !type) return null;
const value = typeof rec.value === "string" || typeof rec.value === "number" || typeof rec.value === "boolean" ? rec.value : void 0;
return value === void 0 ? {
ref,
type
} : {
ref,
type,
value
};
}).filter((field) => field !== null);
if (!fields.length) return jsonError(res, 400, "fields are required");
const timeoutMs = toNumber(body.timeoutMs);
await pw.fillFormViaPlaywright({
cdpUrl,
targetId: tab.targetId,
fields,
timeoutMs: timeoutMs ?? void 0
});
return res.json({
ok: true,
targetId: tab.targetId
});
}
case "resize": {
const width = toNumber(body.width);
const height = toNumber(body.height);
if (!width || !height) return jsonError(res, 400, "width and height are required");
await pw.resizeViewportViaPlaywright({
cdpUrl,
targetId: tab.targetId,
width,
height
});
return res.json({
ok: true,
targetId: tab.targetId,
url: tab.url
});
}
case "wait": {
const timeMs = toNumber(body.timeMs);
const text = toStringOrEmpty(body.text) || void 0;
const textGone = toStringOrEmpty(body.textGone) || void 0;
const selector = toStringOrEmpty(body.selector) || void 0;
const url = toStringOrEmpty(body.url) || void 0;
const loadStateRaw = toStringOrEmpty(body.loadState);
const loadState = loadStateRaw === "load" || loadStateRaw === "domcontentloaded" || loadStateRaw === "networkidle" ? loadStateRaw : void 0;
const fn = toStringOrEmpty(body.fn) || void 0;
const timeoutMs = toNumber(body.timeoutMs) ?? void 0;
if (fn && !evaluateEnabled) return jsonError(res, 403, ["wait --fn is disabled by config (browser.evaluateEnabled=false).", "Docs: /gateway/configuration#browser-openclaw-managed-browser"].join("\n"));
if (timeMs === void 0 && !text && !textGone && !selector && !url && !loadState && !fn) return jsonError(res, 400, "wait requires at least one of: timeMs, text, textGone, selector, url, loadState, fn");
await pw.waitForViaPlaywright({
cdpUrl,
targetId: tab.targetId,
timeMs,
text,
textGone,
selector,
url,
loadState,
fn,
timeoutMs
});
return res.json({
ok: true,
targetId: tab.targetId
});
}
case "evaluate": {
if (!evaluateEnabled) return jsonError(res, 403, ["act:evaluate is disabled by config (browser.evaluateEnabled=false).", "Docs: /gateway/configuration#browser-openclaw-managed-browser"].join("\n"));
const fn = toStringOrEmpty(body.fn);
if (!fn) return jsonError(res, 400, "fn is required");
const ref = toStringOrEmpty(body.ref) || void 0;
const evalTimeoutMs = toNumber(body.timeoutMs);
const evalRequest = {
cdpUrl,
targetId: tab.targetId,
fn,
ref,
signal: req.signal
};
if (evalTimeoutMs !== void 0) evalRequest.timeoutMs = evalTimeoutMs;
const result = await pw.evaluateViaPlaywright(evalRequest);
return res.json({
ok: true,
targetId: tab.targetId,
url: tab.url,
result
});
}
case "close":
await pw.closePageViaPlaywright({
cdpUrl,
targetId: tab.targetId
});
return res.json({
ok: true,
targetId: tab.targetId
});
default: return jsonError(res, 400, "unsupported kind");
}
}
});
});
app.post("/hooks/file-chooser", async (req, res) => {
const body = readBody(req);
const targetId = resolveTargetIdFromBody(body);
const ref = toStringOrEmpty(body.ref) || void 0;
const inputRef = toStringOrEmpty(body.inputRef) || void 0;
const element = toStringOrEmpty(body.element) || void 0;
const paths = toStringArray(body.paths) ?? [];
const timeoutMs = toNumber(body.timeoutMs);
if (!paths.length) return jsonError(res, 400, "paths are required");
await withPlaywrightRouteContext({
req,
res,
ctx,
targetId,
feature: "file chooser hook",
run: async ({ cdpUrl, tab, pw }) => {
c