UNPKG

@gguf/claw

Version:

WhatsApp gateway CLI (Baileys web) with Pi RPC agent

1,447 lines (1,425 loc) 300 kB
import { i as resolveGatewayPort, t as STATE_DIR } from "./paths-B1kfl4h5.js"; import { A as normalizeAccountId$1, C as resolveOpenClawPackageRootSync, D as buildAgentMainSessionKey, M as normalizeMainKey, N as resolveAgentIdFromSessionKey, T as DEFAULT_AGENT_ID, _ as DEFAULT_TOOLS_FILENAME, c as resolveDefaultAgentId, d as DEFAULT_AGENTS_FILENAME, f as DEFAULT_AGENT_WORKSPACE_DIR, g as DEFAULT_SOUL_FILENAME, h as DEFAULT_IDENTITY_FILENAME, j as normalizeAgentId, l as resolveSessionAgentId, m as DEFAULT_HEARTBEAT_FILENAME, n as resolveAgentConfig, p as DEFAULT_BOOTSTRAP_FILENAME, v as DEFAULT_USER_FILENAME, w as DEFAULT_ACCOUNT_ID, y as ensureAgentWorkspace } from "./agent-scope-Csu2B6AM.js"; import { A as normalizeE164, C as CONFIG_DIR, N as resolveUserPath, _ as normalizeAnyChannelId, b as getActivePluginRegistry, g as getChatChannelMeta, h as CHAT_CHANNEL_ORDER, j as resolveConfigDir, l as createSubsystemLogger, m as CHANNEL_IDS, n as runExec, u as defaultRuntime, x as requireActivePluginRegistry, y as normalizeChatChannelId } from "./exec-BMnoMcZW.js"; import { O as isTruthyEnvValue, k as parseBooleanValue } from "./model-selection-mzTqrNoj.js"; import { t as formatCliCommand } from "./command-format-CFzL448l.js"; import { _ as resolveEnableState, f as loadPluginManifestRegistry, g as normalizePluginsConfig, h as MANIFEST_KEY, i as writeConfigFile, k as resolveWhatsAppAccount, m as LEGACY_MANIFEST_KEYS, t as loadConfig, v as resolveMemorySlotDecision } from "./config-CG73z4h6.js"; import { C as DEFAULT_AI_SNAPSHOT_MAX_CHARS, D as DEFAULT_OPENCLAW_BROWSER_ENABLED, E as DEFAULT_OPENCLAW_BROWSER_COLOR, O as DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME, S as DEFAULT_AI_SNAPSHOT_EFFICIENT_MAX_CHARS, T as DEFAULT_BROWSER_EVALUATE_ENABLED, a as resolveOpenClawUserDataDir, c as captureScreenshot, d as normalizeCdpWsUrl, f as snapshotAria, g as stopChromeExtensionRelayServer, h as ensureChromeExtensionRelayServer, i as launchOpenClawChrome, l as createTargetViaCdp, m as getHeadersWithAuth, n as isChromeCdpReady, o as stopOpenClawChrome, p as appendCdpPath, r as isChromeReachable, s as resolveBrowserExecutableForPlatform, v as extractErrorCode, w as DEFAULT_BROWSER_DEFAULT_PROFILE_NAME, x as DEFAULT_AI_SNAPSHOT_EFFICIENT_DEPTH, y as formatErrorMessage } from "./chrome-B3IuUad-.js"; import { r as resolveSessionTranscriptPath, t as resolveDefaultSessionStorePath } from "./paths-B4kigINg.js"; import { t as emitSessionTranscriptUpdate } from "./transcript-events-JLH5W4He.js"; import os from "node:os"; import path from "node:path"; import fs from "node:fs"; import JSON5 from "json5"; import fs$1 from "node:fs/promises"; import { spawn } from "node:child_process"; import { fileURLToPath } from "node:url"; import crypto, { createHash } from "node:crypto"; import { CURRENT_SESSION_VERSION, SessionManager, formatSkillsForPrompt, loadSkillsFromDir } from "@mariozechner/pi-coding-agent"; import { fileTypeFromBuffer } from "file-type"; import YAML from "yaml"; import express from "express"; import { lookup } from "node:dns"; import { lookup as lookup$1 } from "node:dns/promises"; import { Agent } from "undici"; //#region src/discord/token.ts function normalizeDiscordToken(raw) { if (!raw) return; const trimmed = raw.trim(); if (!trimmed) return; return trimmed.replace(/^Bot\s+/i, ""); } function resolveDiscordToken(cfg, opts = {}) { const accountId = normalizeAccountId$1(opts.accountId); const discordCfg = cfg?.channels?.discord; const accountToken = normalizeDiscordToken((accountId !== DEFAULT_ACCOUNT_ID ? discordCfg?.accounts?.[accountId] : discordCfg?.accounts?.[DEFAULT_ACCOUNT_ID])?.token ?? void 0); if (accountToken) return { token: accountToken, source: "config" }; const allowEnv = accountId === DEFAULT_ACCOUNT_ID; const configToken = allowEnv ? normalizeDiscordToken(discordCfg?.token ?? void 0) : void 0; if (configToken) return { token: configToken, source: "config" }; const envToken = allowEnv ? normalizeDiscordToken(opts.envToken ?? process.env.DISCORD_BOT_TOKEN) : void 0; if (envToken) return { token: envToken, source: "env" }; return { token: "", source: "none" }; } //#endregion //#region src/discord/accounts.ts function listConfiguredAccountIds$2(cfg) { const accounts = cfg.channels?.discord?.accounts; if (!accounts || typeof accounts !== "object") return []; return Object.keys(accounts).filter(Boolean); } function listDiscordAccountIds(cfg) { const ids = listConfiguredAccountIds$2(cfg); if (ids.length === 0) return [DEFAULT_ACCOUNT_ID]; return ids.toSorted((a, b) => a.localeCompare(b)); } function resolveAccountConfig$4(cfg, accountId) { const accounts = cfg.channels?.discord?.accounts; if (!accounts || typeof accounts !== "object") return; return accounts[accountId]; } function mergeDiscordAccountConfig(cfg, accountId) { const { accounts: _ignored, ...base } = cfg.channels?.discord ?? {}; const account = resolveAccountConfig$4(cfg, accountId) ?? {}; return { ...base, ...account }; } function resolveDiscordAccount(params) { const accountId = normalizeAccountId$1(params.accountId); const baseEnabled = params.cfg.channels?.discord?.enabled !== false; const merged = mergeDiscordAccountConfig(params.cfg, accountId); const accountEnabled = merged.enabled !== false; const enabled = baseEnabled && accountEnabled; const tokenResolution = resolveDiscordToken(params.cfg, { accountId }); return { accountId, enabled, name: merged.name?.trim() || void 0, token: tokenResolution.token, tokenSource: tokenResolution.source, config: merged }; } function listEnabledDiscordAccounts(cfg) { return listDiscordAccountIds(cfg).map((accountId) => resolveDiscordAccount({ cfg, accountId })).filter((account) => account.enabled); } //#endregion //#region src/channels/chat-type.ts function normalizeChatType(raw) { const value = raw?.trim().toLowerCase(); if (!value) return; if (value === "direct" || value === "dm") return "direct"; if (value === "group") return "group"; if (value === "channel") return "channel"; } //#endregion //#region src/slack/token.ts function normalizeSlackToken(raw) { const trimmed = raw?.trim(); return trimmed ? trimmed : void 0; } function resolveSlackBotToken(raw) { return normalizeSlackToken(raw); } function resolveSlackAppToken(raw) { return normalizeSlackToken(raw); } //#endregion //#region src/slack/accounts.ts function resolveAccountConfig$3(cfg, accountId) { const accounts = cfg.channels?.slack?.accounts; if (!accounts || typeof accounts !== "object") return; return accounts[accountId]; } function mergeSlackAccountConfig(cfg, accountId) { const { accounts: _ignored, ...base } = cfg.channels?.slack ?? {}; const account = resolveAccountConfig$3(cfg, accountId) ?? {}; return { ...base, ...account }; } function resolveSlackAccount(params) { const accountId = normalizeAccountId$1(params.accountId); const baseEnabled = params.cfg.channels?.slack?.enabled !== false; const merged = mergeSlackAccountConfig(params.cfg, accountId); const accountEnabled = merged.enabled !== false; const enabled = baseEnabled && accountEnabled; const allowEnv = accountId === DEFAULT_ACCOUNT_ID; const envBot = allowEnv ? resolveSlackBotToken(process.env.SLACK_BOT_TOKEN) : void 0; const envApp = allowEnv ? resolveSlackAppToken(process.env.SLACK_APP_TOKEN) : void 0; const configBot = resolveSlackBotToken(merged.botToken); const configApp = resolveSlackAppToken(merged.appToken); const botToken = configBot ?? envBot; const appToken = configApp ?? envApp; const botTokenSource = configBot ? "config" : envBot ? "env" : "none"; const appTokenSource = configApp ? "config" : envApp ? "env" : "none"; return { accountId, enabled, name: merged.name?.trim() || void 0, botToken, appToken, botTokenSource, appTokenSource, config: merged, groupPolicy: merged.groupPolicy, textChunkLimit: merged.textChunkLimit, mediaMaxMb: merged.mediaMaxMb, reactionNotifications: merged.reactionNotifications, reactionAllowlist: merged.reactionAllowlist, replyToMode: merged.replyToMode, replyToModeByChatType: merged.replyToModeByChatType, actions: merged.actions, slashCommand: merged.slashCommand, dm: merged.dm, channels: merged.channels }; } function resolveSlackReplyToMode(account, chatType) { const normalized = normalizeChatType(chatType ?? void 0); if (normalized && account.replyToModeByChatType?.[normalized] !== void 0) return account.replyToModeByChatType[normalized] ?? "off"; if (normalized === "direct" && account.dm?.replyToMode !== void 0) return account.dm.replyToMode; return account.replyToMode ?? "off"; } //#endregion //#region src/routing/bindings.ts function normalizeBindingChannelId(raw) { const normalized = normalizeChatChannelId(raw); if (normalized) return normalized; return (raw ?? "").trim().toLowerCase() || null; } function listBindings(cfg) { return Array.isArray(cfg.bindings) ? cfg.bindings : []; } function listBoundAccountIds(cfg, channelId) { const normalizedChannel = normalizeBindingChannelId(channelId); if (!normalizedChannel) return []; const ids = /* @__PURE__ */ new Set(); for (const binding of listBindings(cfg)) { if (!binding || typeof binding !== "object") continue; const match = binding.match; if (!match || typeof match !== "object") continue; const channel = normalizeBindingChannelId(match.channel); if (!channel || channel !== normalizedChannel) continue; const accountId = typeof match.accountId === "string" ? match.accountId.trim() : ""; if (!accountId || accountId === "*") continue; ids.add(normalizeAccountId$1(accountId)); } return Array.from(ids).toSorted((a, b) => a.localeCompare(b)); } function resolveDefaultAgentBoundAccountId(cfg, channelId) { const normalizedChannel = normalizeBindingChannelId(channelId); if (!normalizedChannel) return null; const defaultAgentId = normalizeAgentId(resolveDefaultAgentId(cfg)); for (const binding of listBindings(cfg)) { if (!binding || typeof binding !== "object") continue; if (normalizeAgentId(binding.agentId) !== defaultAgentId) continue; const match = binding.match; if (!match || typeof match !== "object") continue; const channel = normalizeBindingChannelId(match.channel); if (!channel || channel !== normalizedChannel) continue; const accountId = typeof match.accountId === "string" ? match.accountId.trim() : ""; if (!accountId || accountId === "*") continue; return normalizeAccountId$1(accountId); } return null; } //#endregion //#region src/telegram/token.ts function resolveTelegramToken(cfg, opts = {}) { const accountId = normalizeAccountId$1(opts.accountId); const telegramCfg = cfg?.channels?.telegram; const resolveAccountCfg = (id) => { const accounts = telegramCfg?.accounts; if (!accounts || typeof accounts !== "object" || Array.isArray(accounts)) return; const direct = accounts[id]; if (direct) return direct; const matchKey = Object.keys(accounts).find((key) => normalizeAccountId$1(key) === id); return matchKey ? accounts[matchKey] : void 0; }; const accountCfg = resolveAccountCfg(accountId !== DEFAULT_ACCOUNT_ID ? accountId : DEFAULT_ACCOUNT_ID); const accountTokenFile = accountCfg?.tokenFile?.trim(); if (accountTokenFile) { if (!fs.existsSync(accountTokenFile)) { opts.logMissingFile?.(`channels.telegram.accounts.${accountId}.tokenFile not found: ${accountTokenFile}`); return { token: "", source: "none" }; } try { const token = fs.readFileSync(accountTokenFile, "utf-8").trim(); if (token) return { token, source: "tokenFile" }; } catch (err) { opts.logMissingFile?.(`channels.telegram.accounts.${accountId}.tokenFile read failed: ${String(err)}`); return { token: "", source: "none" }; } return { token: "", source: "none" }; } const accountToken = accountCfg?.botToken?.trim(); if (accountToken) return { token: accountToken, source: "config" }; const allowEnv = accountId === DEFAULT_ACCOUNT_ID; const tokenFile = telegramCfg?.tokenFile?.trim(); if (tokenFile && allowEnv) { if (!fs.existsSync(tokenFile)) { opts.logMissingFile?.(`channels.telegram.tokenFile not found: ${tokenFile}`); return { token: "", source: "none" }; } try { const token = fs.readFileSync(tokenFile, "utf-8").trim(); if (token) return { token, source: "tokenFile" }; } catch (err) { opts.logMissingFile?.(`channels.telegram.tokenFile read failed: ${String(err)}`); return { token: "", source: "none" }; } } const configToken = telegramCfg?.botToken?.trim(); if (configToken && allowEnv) return { token: configToken, source: "config" }; const envToken = allowEnv ? (opts.envToken ?? process.env.TELEGRAM_BOT_TOKEN)?.trim() : ""; if (envToken) return { token: envToken, source: "env" }; return { token: "", source: "none" }; } //#endregion //#region src/telegram/accounts.ts const debugAccounts = (...args) => { if (isTruthyEnvValue(process.env.OPENCLAW_DEBUG_TELEGRAM_ACCOUNTS)) console.warn("[telegram:accounts]", ...args); }; function listConfiguredAccountIds$1(cfg) { const accounts = cfg.channels?.telegram?.accounts; if (!accounts || typeof accounts !== "object") return []; const ids = /* @__PURE__ */ new Set(); for (const key of Object.keys(accounts)) { if (!key) continue; ids.add(normalizeAccountId$1(key)); } return [...ids]; } function listTelegramAccountIds(cfg) { const ids = Array.from(new Set([...listConfiguredAccountIds$1(cfg), ...listBoundAccountIds(cfg, "telegram")])); debugAccounts("listTelegramAccountIds", ids); if (ids.length === 0) return [DEFAULT_ACCOUNT_ID]; return ids.toSorted((a, b) => a.localeCompare(b)); } function resolveDefaultTelegramAccountId(cfg) { const boundDefault = resolveDefaultAgentBoundAccountId(cfg, "telegram"); if (boundDefault) return boundDefault; const ids = listTelegramAccountIds(cfg); if (ids.includes(DEFAULT_ACCOUNT_ID)) return DEFAULT_ACCOUNT_ID; return ids[0] ?? DEFAULT_ACCOUNT_ID; } function resolveAccountConfig$2(cfg, accountId) { const accounts = cfg.channels?.telegram?.accounts; if (!accounts || typeof accounts !== "object") return; const direct = accounts[accountId]; if (direct) return direct; const normalized = normalizeAccountId$1(accountId); const matchKey = Object.keys(accounts).find((key) => normalizeAccountId$1(key) === normalized); return matchKey ? accounts[matchKey] : void 0; } function mergeTelegramAccountConfig(cfg, accountId) { const { accounts: _ignored, ...base } = cfg.channels?.telegram ?? {}; const account = resolveAccountConfig$2(cfg, accountId) ?? {}; return { ...base, ...account }; } function resolveTelegramAccount(params) { const hasExplicitAccountId = Boolean(params.accountId?.trim()); const baseEnabled = params.cfg.channels?.telegram?.enabled !== false; const resolve = (accountId) => { const merged = mergeTelegramAccountConfig(params.cfg, accountId); const accountEnabled = merged.enabled !== false; const enabled = baseEnabled && accountEnabled; const tokenResolution = resolveTelegramToken(params.cfg, { accountId }); debugAccounts("resolve", { accountId, enabled, tokenSource: tokenResolution.source }); return { accountId, enabled, name: merged.name?.trim() || void 0, token: tokenResolution.token, tokenSource: tokenResolution.source, config: merged }; }; const primary = resolve(normalizeAccountId$1(params.accountId)); if (hasExplicitAccountId) return primary; if (primary.tokenSource !== "none") return primary; const fallbackId = resolveDefaultTelegramAccountId(params.cfg); if (fallbackId === primary.accountId) return primary; const fallback = resolve(fallbackId); if (fallback.tokenSource === "none") return primary; return fallback; } function listEnabledTelegramAccounts(cfg) { return listTelegramAccountIds(cfg).map((accountId) => resolveTelegramAccount({ cfg, accountId })).filter((account) => account.enabled); } //#endregion //#region src/whatsapp/normalize.ts const WHATSAPP_USER_JID_RE = /^(\d+)(?::\d+)?@s\.whatsapp\.net$/i; const WHATSAPP_LID_RE = /^(\d+)@lid$/i; function stripWhatsAppTargetPrefixes(value) { let candidate = value.trim(); for (;;) { const before = candidate; candidate = candidate.replace(/^whatsapp:/i, "").trim(); if (candidate === before) return candidate; } } function isWhatsAppGroupJid(value) { const candidate = stripWhatsAppTargetPrefixes(value); if (!candidate.toLowerCase().endsWith("@g.us")) return false; const localPart = candidate.slice(0, candidate.length - 5); if (!localPart || localPart.includes("@")) return false; return /^[0-9]+(-[0-9]+)*$/.test(localPart); } /** * Check if value looks like a WhatsApp user target (e.g. "41796666864:0@s.whatsapp.net" or "123@lid"). */ function isWhatsAppUserTarget(value) { const candidate = stripWhatsAppTargetPrefixes(value); return WHATSAPP_USER_JID_RE.test(candidate) || WHATSAPP_LID_RE.test(candidate); } /** * Extract the phone number from a WhatsApp user JID. * "41796666864:0@s.whatsapp.net" -> "41796666864" * "123456@lid" -> "123456" */ function extractUserJidPhone(jid) { const userMatch = jid.match(WHATSAPP_USER_JID_RE); if (userMatch) return userMatch[1]; const lidMatch = jid.match(WHATSAPP_LID_RE); if (lidMatch) return lidMatch[1]; return null; } function normalizeWhatsAppTarget(value) { const candidate = stripWhatsAppTargetPrefixes(value); if (!candidate) return null; if (isWhatsAppGroupJid(candidate)) return `${candidate.slice(0, candidate.length - 5)}@g.us`; if (isWhatsAppUserTarget(candidate)) { const phone = extractUserJidPhone(candidate); if (!phone) return null; const normalized = normalizeE164(phone); return normalized.length > 1 ? normalized : null; } if (candidate.includes("@")) return null; const normalized = normalizeE164(candidate); return normalized.length > 1 ? normalized : null; } //#endregion //#region src/channels/plugins/index.ts function listPluginChannels() { return requireActivePluginRegistry().channels.map((entry) => entry.plugin); } function dedupeChannels(channels) { const seen = /* @__PURE__ */ new Set(); const resolved = []; for (const plugin of channels) { const id = String(plugin.id).trim(); if (!id || seen.has(id)) continue; seen.add(id); resolved.push(plugin); } return resolved; } function listChannelPlugins() { return dedupeChannels(listPluginChannels()).toSorted((a, b) => { const indexA = CHAT_CHANNEL_ORDER.indexOf(a.id); const indexB = CHAT_CHANNEL_ORDER.indexOf(b.id); const orderA = a.meta.order ?? (indexA === -1 ? 999 : indexA); const orderB = b.meta.order ?? (indexB === -1 ? 999 : indexB); if (orderA !== orderB) return orderA - orderB; return a.id.localeCompare(b.id); }); } function getChannelPlugin(id) { const resolvedId = String(id).trim(); if (!resolvedId) return; return listChannelPlugins().find((plugin) => plugin.id === resolvedId); } function normalizeChannelId(raw) { return normalizeAnyChannelId(raw); } //#endregion //#region src/signal/accounts.ts function listConfiguredAccountIds(cfg) { const accounts = cfg.channels?.signal?.accounts; if (!accounts || typeof accounts !== "object") return []; return Object.keys(accounts).filter(Boolean); } function listSignalAccountIds(cfg) { const ids = listConfiguredAccountIds(cfg); if (ids.length === 0) return [DEFAULT_ACCOUNT_ID]; return ids.toSorted((a, b) => a.localeCompare(b)); } function resolveAccountConfig$1(cfg, accountId) { const accounts = cfg.channels?.signal?.accounts; if (!accounts || typeof accounts !== "object") return; return accounts[accountId]; } function mergeSignalAccountConfig(cfg, accountId) { const { accounts: _ignored, ...base } = cfg.channels?.signal ?? {}; const account = resolveAccountConfig$1(cfg, accountId) ?? {}; return { ...base, ...account }; } function resolveSignalAccount(params) { const accountId = normalizeAccountId$1(params.accountId); const baseEnabled = params.cfg.channels?.signal?.enabled !== false; const merged = mergeSignalAccountConfig(params.cfg, accountId); const accountEnabled = merged.enabled !== false; const enabled = baseEnabled && accountEnabled; const host = merged.httpHost?.trim() || "127.0.0.1"; const port = merged.httpPort ?? 8080; const baseUrl = merged.httpUrl?.trim() || `http://${host}:${port}`; const configured = Boolean(merged.account?.trim() || merged.httpUrl?.trim() || merged.cliPath?.trim() || merged.httpHost?.trim() || typeof merged.httpPort === "number" || typeof merged.autoStart === "boolean"); return { accountId, enabled, name: merged.name?.trim() || void 0, baseUrl, configured, config: merged }; } function listEnabledSignalAccounts(cfg) { return listSignalAccountIds(cfg).map((accountId) => resolveSignalAccount({ cfg, accountId })).filter((account) => account.enabled); } //#endregion //#region src/media/constants.ts const MAX_IMAGE_BYTES$1 = 6 * 1024 * 1024; const MAX_AUDIO_BYTES = 16 * 1024 * 1024; const MAX_VIDEO_BYTES = 16 * 1024 * 1024; const MAX_DOCUMENT_BYTES = 100 * 1024 * 1024; function mediaKindFromMime(mime) { if (!mime) return "unknown"; if (mime.startsWith("image/")) return "image"; if (mime.startsWith("audio/")) return "audio"; if (mime.startsWith("video/")) return "video"; if (mime === "application/pdf") return "document"; if (mime.startsWith("application/")) return "document"; return "unknown"; } function maxBytesForKind(kind) { switch (kind) { case "image": return MAX_IMAGE_BYTES$1; case "audio": return MAX_AUDIO_BYTES; case "video": return MAX_VIDEO_BYTES; case "document": return MAX_DOCUMENT_BYTES; default: return MAX_DOCUMENT_BYTES; } } //#endregion //#region src/media/mime.ts const EXT_BY_MIME = { "image/heic": ".heic", "image/heif": ".heif", "image/jpeg": ".jpg", "image/png": ".png", "image/webp": ".webp", "image/gif": ".gif", "audio/ogg": ".ogg", "audio/mpeg": ".mp3", "audio/x-m4a": ".m4a", "audio/mp4": ".m4a", "video/mp4": ".mp4", "video/quicktime": ".mov", "application/pdf": ".pdf", "application/json": ".json", "application/zip": ".zip", "application/gzip": ".gz", "application/x-tar": ".tar", "application/x-7z-compressed": ".7z", "application/vnd.rar": ".rar", "application/msword": ".doc", "application/vnd.ms-excel": ".xls", "application/vnd.ms-powerpoint": ".ppt", "application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx", "application/vnd.openxmlformats-officedocument.presentationml.presentation": ".pptx", "text/csv": ".csv", "text/plain": ".txt", "text/markdown": ".md" }; const MIME_BY_EXT = { ...Object.fromEntries(Object.entries(EXT_BY_MIME).map(([mime, ext]) => [ext, mime])), ".jpeg": "image/jpeg" }; const AUDIO_FILE_EXTENSIONS = new Set([ ".aac", ".flac", ".m4a", ".mp3", ".oga", ".ogg", ".opus", ".wav" ]); function normalizeHeaderMime(mime) { if (!mime) return; return mime.split(";")[0]?.trim().toLowerCase() || void 0; } async function sniffMime(buffer) { if (!buffer) return; try { return (await fileTypeFromBuffer(buffer))?.mime ?? void 0; } catch { return; } } function getFileExtension(filePath) { if (!filePath) return; try { if (/^https?:\/\//i.test(filePath)) { const url = new URL(filePath); return path.extname(url.pathname).toLowerCase() || void 0; } } catch {} return path.extname(filePath).toLowerCase() || void 0; } function isAudioFileName(fileName) { const ext = getFileExtension(fileName); if (!ext) return false; return AUDIO_FILE_EXTENSIONS.has(ext); } function detectMime(opts) { return detectMimeImpl(opts); } function isGenericMime(mime) { if (!mime) return true; const m = mime.toLowerCase(); return m === "application/octet-stream" || m === "application/zip"; } async function detectMimeImpl(opts) { const ext = getFileExtension(opts.filePath); const extMime = ext ? MIME_BY_EXT[ext] : void 0; const headerMime = normalizeHeaderMime(opts.headerMime); const sniffed = await sniffMime(opts.buffer); if (sniffed && (!isGenericMime(sniffed) || !extMime)) return sniffed; if (extMime) return extMime; if (headerMime && !isGenericMime(headerMime)) return headerMime; if (sniffed) return sniffed; if (headerMime) return headerMime; } function extensionForMime(mime) { if (!mime) return; return EXT_BY_MIME[mime.toLowerCase()]; } function isGifMedia(opts) { if (opts.contentType?.toLowerCase() === "image/gif") return true; return getFileExtension(opts.fileName) === ".gif"; } function imageMimeFromFormat(format) { if (!format) return; switch (format.toLowerCase()) { case "jpg": case "jpeg": return "image/jpeg"; case "heic": return "image/heic"; case "heif": return "image/heif"; case "png": return "image/png"; case "webp": return "image/webp"; case "gif": return "image/gif"; default: return; } } function kindFromMime(mime) { return mediaKindFromMime(mime); } //#endregion //#region src/gateway/protocol/client-info.ts const GATEWAY_CLIENT_IDS = { WEBCHAT_UI: "webchat-ui", CONTROL_UI: "openclaw-control-ui", WEBCHAT: "webchat", CLI: "cli", GATEWAY_CLIENT: "gateway-client", MACOS_APP: "openclaw-macos", IOS_APP: "openclaw-ios", ANDROID_APP: "openclaw-android", NODE_HOST: "node-host", TEST: "test", FINGERPRINT: "fingerprint", PROBE: "openclaw-probe" }; const GATEWAY_CLIENT_NAMES = GATEWAY_CLIENT_IDS; const GATEWAY_CLIENT_MODES = { WEBCHAT: "webchat", CLI: "cli", UI: "ui", BACKEND: "backend", NODE: "node", PROBE: "probe", TEST: "test" }; const GATEWAY_CLIENT_ID_SET = new Set(Object.values(GATEWAY_CLIENT_IDS)); const GATEWAY_CLIENT_MODE_SET = new Set(Object.values(GATEWAY_CLIENT_MODES)); //#endregion //#region src/utils/message-channel.ts const INTERNAL_MESSAGE_CHANNEL = "webchat"; const MARKDOWN_CAPABLE_CHANNELS = new Set([ "slack", "telegram", "signal", "discord", "googlechat", "tui", INTERNAL_MESSAGE_CHANNEL ]); function isInternalMessageChannel(raw) { return normalizeMessageChannel(raw) === INTERNAL_MESSAGE_CHANNEL; } function normalizeMessageChannel(raw) { const normalized = raw?.trim().toLowerCase(); if (!normalized) return; if (normalized === INTERNAL_MESSAGE_CHANNEL) return INTERNAL_MESSAGE_CHANNEL; const builtIn = normalizeChatChannelId(normalized); if (builtIn) return builtIn; return (getActivePluginRegistry()?.channels.find((entry) => { if (entry.plugin.id.toLowerCase() === normalized) return true; return (entry.plugin.meta.aliases ?? []).some((alias) => alias.trim().toLowerCase() === normalized); }))?.plugin.id ?? normalized; } const listPluginChannelIds = () => { const registry = getActivePluginRegistry(); if (!registry) return []; return registry.channels.map((entry) => entry.plugin.id); }; const listDeliverableMessageChannels = () => Array.from(new Set([...CHANNEL_IDS, ...listPluginChannelIds()])); const listGatewayMessageChannels = () => [...listDeliverableMessageChannels(), INTERNAL_MESSAGE_CHANNEL]; function isGatewayMessageChannel(value) { return listGatewayMessageChannels().includes(value); } function isDeliverableMessageChannel(value) { return listDeliverableMessageChannels().includes(value); } function resolveGatewayMessageChannel(raw) { const normalized = normalizeMessageChannel(raw); if (!normalized) return; return isGatewayMessageChannel(normalized) ? normalized : void 0; } function resolveMessageChannel(primary, fallback) { return normalizeMessageChannel(primary) ?? normalizeMessageChannel(fallback); } function isMarkdownCapableMessageChannel(raw) { const channel = normalizeMessageChannel(raw); if (!channel) return false; return MARKDOWN_CAPABLE_CHANNELS.has(channel); } //#endregion //#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 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 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 }; } 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; const result = []; for (const file of files) { if (file.missing) { result.push({ path: file.name, content: `[MISSING] Expected at: ${file.path}` }); continue; } const trimmed = trimBootstrapContent(file.content ?? "", file.name, maxChars); if (!trimmed.content) continue; if (trimmed.truncated) opts?.warn?.(`workspace bootstrap file ${file.name} is ${trimmed.originalLength} chars (limit ${trimmed.maxChars}); truncating in injected context`); result.push({ path: file.name, content: trimmed.content }); } 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(os.homedir(), ".openclaw", "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", "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 resolvedSandboxStateDir = STATE_DIR ?? path.join(os.homedir(), ".openclaw"); const SANDBOX_STATE_DIR = path.join(resolvedSandboxStateDir, "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/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", "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", "session_status", "memory_search", "memory_get", "web_search", "web_fetch", "image" ] }; const OWNER_ONLY_TOOL_NAMES = new Set(["whatsapp_login"]); 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_NAMES.has(normalizeToolName(name)); } function applyOwnerOnlyToolPolicy(tools, senderIsOwner) { const withGuard = tools.map((tool) => { if (!isOwnerOnlyToolName(tool.name)) return tool; if (senderIsOwner || !tool.execute) return tool; return { ...tool, execute: async () => { throw new Error("Tool restricted to owner senders."); } }; }); if (senderIsOwner) return withGuard; return withGuard.filter((tool) => !isOwnerOnlyToolName(tool.name)); } 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 }; } //#endregion //#region src/agents/sandbox/tool-policy.ts function compilePattern(pattern) { const normalized = pattern.trim().toLowerCase(); if (!normalized) return { kind: "exact", value: "" }; if (normalized === "*") return { kind: "all" }; if (!normalized.includes("*")) return { kind: "exact", value: normalized }; const escaped = normalized.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); return { kind: "regex", value: new RegExp(`^${escaped.replaceAll("\\*", ".*")}$`) }; } function compilePatterns(patterns) { if (!Array.isArray(patterns)) return []; return expandToolGroups(patterns).map(compilePattern).filter((pattern) => pattern.kind !== "exact" || pattern.value); } function matchesAny(name, patterns) { for (const pattern of patterns) { if (pattern.kind === "all") return true; if (pattern.kind === "exact" && name === pattern.value) return true; if (pattern.kind === "regex" && pattern.value.test(name)) return true; } return false; } function isToolAllowed(policy, name) { const normalized = name.trim().toLowerCase(); if (matchesAny(normalized, compilePatterns(policy.deny))) return false; const allow = compilePatterns(policy.allow); if (allow.length === 0) return true; return matchesAny(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 (!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 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; 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 }; } 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/markdown/frontmatter.ts function stripQuotes(value) { if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1); return value; } function coerceFrontmatterValue(value) { if (value === null || value === void 0) return; if (typeof value === "string") return value.trim(); if (typeof value === "number" || typeof value === "boolean") return String(value); if (typeof value === "object") try { return JSON.stringify(value); } catch { return; } } function parseYamlFrontmatter(block) { try { const parsed = YAML.parse(block); if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null; const result = {}; for (const [rawKey, value] of Object.entries(parsed)) { const key = rawKey.trim(); if (!key) continue; const coerced = coerceFrontmatterValue(value); if (coerced === void 0) continue; result[key] = coerced; } return result; } catch { return null; } } function extractMultiLineValue(lines, startIndex) { const match = lines[startIndex].match(/^([\w-]+):\s*(.*)$/); if (!match) return { value: "", linesConsumed: 1 }; const inlineValue = match[2].trim(); if (inlineValue) return { value: inlineValue, linesConsumed: 1 }; const valueLines = []; let i = startIndex + 1; while (i < lines.length) { const line = lines[i]; if (line.length > 0 && !line.startsWith(" ") && !line.startsWith(" ")) break; valueLines.push(line); i++; } return { value: valueLines.join("\n").trim(), linesConsumed: i - startIndex }; } function parseLineFrontmatter(block) { const frontmatter = {}; const lines = block.split("\n"); let i = 0; while (i < lines.length) { const match = lines[i].match(/^([\w-]+):\s*(.*)$/); if (!match) { i++; continue; } const key = match[1]; const inlineValue = match[2].trim(); if (!key) { i++; continue; } if (!inlineValue && i + 1 < lines.length) { const nextLine = lines[i + 1]; if (nextLine.startsWith(" ") || nextLine.startsWith(" ")) { const { value, linesConsumed } = extractMultiLineValue(lines, i); if (value) frontmatter[key] = value; i += linesConsumed; continue; } } const value = stripQuotes(inlineValue); if (value) frontmatter[key] = value; i++; } return frontmatter; } function parseFrontmatterBlock(content) { const normalized = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); if (!normalized.startsWith("---")) return {}; const endIndex = normalized.indexOf("\n---", 3); if (endIndex === -1) return {}; const block = normalized.slice(4, endIndex); const lineParsed = parseLineFrontmatter(block); const yamlParsed = parseYamlFrontmatter(block); if (yamlParsed === null) return lineParsed; const merged = { ...yamlParsed }; for (const [key, value] of Object.entries(lineParsed)) if (value.startsWith("{") || value.startsWith("[")) merged[key] = value; return merged; } //#endregion //#region src/agents/skills/frontmatter.ts function parseFrontmatter(content) { return parseFrontmatterBlock(content); } function normalizeStringList(input) { if (!input) return []; if (Array.isArray(input)) return input.map((value) => String(value).trim()).filter(Boolean); if (typeof input === "string") return input.split(",").map((value) => value.trim()).filter(Boolean); return []; } function parseInstallSpec(input) { if (!input || typeof input !== "object") return; c