UNPKG

@gguf/claw

Version:

Multi-channel AI gateway with extensible messaging integrations

1,447 lines (1,430 loc) 268 kB
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