UNPKG

@gguf/claw

Version:

WhatsApp gateway CLI (Baileys web) with Pi RPC agent

1,397 lines (1,381 loc) 108 kB
import { L as STATE_DIR, Q as CHAT_CHANNEL_ORDER, Z as CHANNEL_IDS, ct as requireActivePluginRegistry, nt as getChatChannelMeta, p as defaultRuntime } from "./entry.js"; import { t as formatCliCommand } from "./command-format-ayFsmwwz.js"; import { c as normalizeAgentId, i as buildAgentMainSessionKey, l as normalizeMainKey, n as DEFAULT_AGENT_ID, s as normalizeAccountId$1, t as DEFAULT_ACCOUNT_ID, u as resolveAgentIdFromSessionKey } from "./session-key-CZkcvAtx.js"; import { m as resolveUserPath, u as normalizeE164 } from "./utils-DX85MiPR.js"; import { b as DEFAULT_USER_FILENAME, d as DEFAULT_AGENTS_FILENAME, f as DEFAULT_AGENT_WORKSPACE_DIR, h as DEFAULT_IDENTITY_FILENAME, l as resolveSessionAgentId, m as DEFAULT_HEARTBEAT_FILENAME, n as resolveAgentConfig, p as DEFAULT_BOOTSTRAP_FILENAME, v as DEFAULT_SOUL_FILENAME, x as ensureAgentWorkspace, y as DEFAULT_TOOLS_FILENAME } from "./agent-scope-C9VjJXEK.js"; import { i as loadConfig } from "./config-CKLedg5Y.js"; import { o as resolveProfile, t as createBrowserRouteContext } from "./server-context-yKyxyxOJ.js"; import { O as DEFAULT_BROWSER_EVALUATE_ENABLED, j as DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME, k as DEFAULT_OPENCLAW_BROWSER_COLOR } from "./errors-CZ9opC6L.js"; import { c as listDeliverableMessageChannels, l as normalizeMessageChannel } from "./message-channel-BlgPSDAh.js"; import { _ as normalizeChatType, a as normalizeWhatsAppTarget, c as resolveTelegramAccount, m as resolveSlackReplyToMode, p as resolveSlackAccount, r as normalizeChannelId, y as resolveDiscordAccount } from "./plugins-BUPpq5aS.js"; import { n as resolveWhatsAppAccount } from "./accounts-Dto4p9zB.js"; import { r as resolveSessionTranscriptPath, t as resolveDefaultSessionStorePath } from "./paths-CTg8F3AE.js"; import { t as emitSessionTranscriptUpdate } from "./transcript-events-CZ8CG4ht.js"; import { t as registerBrowserRoutes } from "./routes-BSfXf8a5.js"; import { o as syncSkillsToWorkspace } from "./skills-CmU0Q92f.js"; import { spawn } from "node:child_process"; import path from "node:path"; import os from "node:os"; import fs from "node:fs"; import JSON5 from "json5"; import fs$1 from "node:fs/promises"; import crypto from "node:crypto"; import { CURRENT_SESSION_VERSION, SessionManager } from "@mariozechner/pi-coding-agent"; import express from "express"; //#region src/imessage/accounts.ts function resolveAccountConfig$1(cfg, accountId) { const accounts = cfg.channels?.imessage?.accounts; if (!accounts || typeof accounts !== "object") return; return accounts[accountId]; } function mergeIMessageAccountConfig(cfg, accountId) { const { accounts: _ignored, ...base } = cfg.channels?.imessage ?? {}; const account = resolveAccountConfig$1(cfg, accountId) ?? {}; return { ...base, ...account }; } function resolveIMessageAccount(params) { const accountId = normalizeAccountId$1(params.accountId); const baseEnabled = params.cfg.channels?.imessage?.enabled !== false; const merged = mergeIMessageAccountConfig(params.cfg, accountId); const accountEnabled = merged.enabled !== false; const configured = Boolean(merged.cliPath?.trim() || merged.dbPath?.trim() || merged.service || merged.region?.trim() || merged.allowFrom && merged.allowFrom.length > 0 || merged.groupAllowFrom && merged.groupAllowFrom.length > 0 || merged.dmPolicy || merged.groupPolicy || typeof merged.includeAttachments === "boolean" || typeof merged.mediaMaxMb === "number" || typeof merged.textChunkLimit === "number" || merged.groups && Object.keys(merged.groups).length > 0); return { accountId, enabled: baseEnabled && accountEnabled, name: merged.name?.trim() || void 0, config: merged, configured }; } //#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(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(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/slack/threading-tool-context.ts function buildSlackThreadingToolContext(params) { const configuredReplyToMode = resolveSlackReplyToMode(resolveSlackAccount({ cfg: params.cfg, accountId: params.accountId }), params.context.ChatType); const effectiveReplyToMode = params.context.ThreadLabel ? "all" : configuredReplyToMode; const threadId = params.context.MessageThreadId ?? params.context.ReplyToId; return { currentChannelId: params.context.To?.startsWith("channel:") ? params.context.To.slice(8) : void 0, currentThreadTs: threadId != null ? String(threadId) : void 0, replyToMode: effectiveReplyToMode, hasRepliedRef: params.hasRepliedRef }; } //#endregion //#region src/config/group-policy.ts function normalizeSenderKey(value) { const trimmed = value.trim(); if (!trimmed) return ""; return (trimmed.startsWith("@") ? trimmed.slice(1) : trimmed).toLowerCase(); } function resolveToolsBySender(params) { const toolsBySender = params.toolsBySender; if (!toolsBySender) return; const entries = Object.entries(toolsBySender); if (entries.length === 0) return; const normalized = /* @__PURE__ */ new Map(); let wildcard; for (const [rawKey, policy] of entries) { if (!policy) continue; const key = normalizeSenderKey(rawKey); if (!key) continue; if (key === "*") { wildcard = policy; continue; } if (!normalized.has(key)) normalized.set(key, policy); } const candidates = []; const pushCandidate = (value) => { const trimmed = value?.trim(); if (!trimmed) return; candidates.push(trimmed); }; pushCandidate(params.senderId); pushCandidate(params.senderE164); pushCandidate(params.senderUsername); pushCandidate(params.senderName); for (const candidate of candidates) { const key = normalizeSenderKey(candidate); if (!key) continue; const match = normalized.get(key); if (match) return match; } return wildcard; } function resolveChannelGroups(cfg, channel, accountId) { const normalizedAccountId = normalizeAccountId$1(accountId); const channelConfig = cfg.channels?.[channel]; if (!channelConfig) return; return channelConfig.accounts?.[normalizedAccountId]?.groups ?? channelConfig.accounts?.[Object.keys(channelConfig.accounts ?? {}).find((key) => key.toLowerCase() === normalizedAccountId.toLowerCase()) ?? ""]?.groups ?? channelConfig.groups; } function resolveChannelGroupPolicy(params) { const { cfg, channel } = params; const groups = resolveChannelGroups(cfg, channel, params.accountId); const allowlistEnabled = Boolean(groups && Object.keys(groups).length > 0); const normalizedId = params.groupId?.trim(); const groupConfig = normalizedId && groups ? groups[normalizedId] : void 0; const defaultConfig = groups?.["*"]; return { allowlistEnabled, allowed: !allowlistEnabled || allowlistEnabled && Boolean(groups && Object.hasOwn(groups, "*")) || (normalizedId ? Boolean(groups && Object.hasOwn(groups, normalizedId)) : false), groupConfig, defaultConfig }; } function resolveChannelGroupRequireMention(params) { const { requireMentionOverride, overrideOrder = "after-config" } = params; const { groupConfig, defaultConfig } = resolveChannelGroupPolicy(params); const configMention = typeof groupConfig?.requireMention === "boolean" ? groupConfig.requireMention : typeof defaultConfig?.requireMention === "boolean" ? defaultConfig.requireMention : void 0; if (overrideOrder === "before-config" && typeof requireMentionOverride === "boolean") return requireMentionOverride; if (typeof configMention === "boolean") return configMention; if (overrideOrder !== "before-config" && typeof requireMentionOverride === "boolean") return requireMentionOverride; return true; } function resolveChannelGroupToolsPolicy(params) { const { groupConfig, defaultConfig } = resolveChannelGroupPolicy(params); const groupSenderPolicy = resolveToolsBySender({ toolsBySender: groupConfig?.toolsBySender, senderId: params.senderId, senderName: params.senderName, senderUsername: params.senderUsername, senderE164: params.senderE164 }); if (groupSenderPolicy) return groupSenderPolicy; if (groupConfig?.tools) return groupConfig.tools; const defaultSenderPolicy = resolveToolsBySender({ toolsBySender: defaultConfig?.toolsBySender, senderId: params.senderId, senderName: params.senderName, senderUsername: params.senderUsername, senderE164: params.senderE164 }); if (defaultSenderPolicy) return defaultSenderPolicy; if (defaultConfig?.tools) return defaultConfig.tools; } //#endregion //#region src/channels/plugins/group-mentions.ts function normalizeDiscordSlug(value) { if (!value) return ""; let text = value.trim().toLowerCase(); if (!text) return ""; text = text.replace(/^[@#]+/, ""); text = text.replace(/[\s_]+/g, "-"); text = text.replace(/[^a-z0-9-]+/g, "-"); text = text.replace(/-{2,}/g, "-").replace(/^-+|-+$/g, ""); return text; } function normalizeSlackSlug(raw) { const trimmed = raw?.trim().toLowerCase() ?? ""; if (!trimmed) return ""; return trimmed.replace(/\s+/g, "-").replace(/[^a-z0-9#@._+-]+/g, "-").replace(/-{2,}/g, "-").replace(/^[-.]+|[-.]+$/g, ""); } function parseTelegramGroupId(value) { const raw = value?.trim() ?? ""; if (!raw) return { chatId: void 0, topicId: void 0 }; const parts = raw.split(":").filter(Boolean); if (parts.length >= 3 && parts[1] === "topic" && /^-?\d+$/.test(parts[0]) && /^\d+$/.test(parts[2])) return { chatId: parts[0], topicId: parts[2] }; if (parts.length >= 2 && /^-?\d+$/.test(parts[0]) && /^\d+$/.test(parts[1])) return { chatId: parts[0], topicId: parts[1] }; return { chatId: raw, topicId: void 0 }; } function resolveTelegramRequireMention(params) { const { cfg, chatId, topicId } = params; if (!chatId) return; const groupConfig = cfg.channels?.telegram?.groups?.[chatId]; const groupDefault = cfg.channels?.telegram?.groups?.["*"]; const topicConfig = topicId && groupConfig?.topics ? groupConfig.topics[topicId] : void 0; const defaultTopicConfig = topicId && groupDefault?.topics ? groupDefault.topics[topicId] : void 0; if (typeof topicConfig?.requireMention === "boolean") return topicConfig.requireMention; if (typeof defaultTopicConfig?.requireMention === "boolean") return defaultTopicConfig.requireMention; if (typeof groupConfig?.requireMention === "boolean") return groupConfig.requireMention; if (typeof groupDefault?.requireMention === "boolean") return groupDefault.requireMention; } function resolveDiscordGuildEntry(guilds, groupSpace) { if (!guilds || Object.keys(guilds).length === 0) return null; const space = groupSpace?.trim() ?? ""; if (space && guilds[space]) return guilds[space]; const normalized = normalizeDiscordSlug(space); if (normalized && guilds[normalized]) return guilds[normalized]; if (normalized) { const match = Object.values(guilds).find((entry) => normalizeDiscordSlug(entry?.slug ?? void 0) === normalized); if (match) return match; } return guilds["*"] ?? null; } function resolveTelegramGroupRequireMention(params) { const { chatId, topicId } = parseTelegramGroupId(params.groupId); const requireMention = resolveTelegramRequireMention({ cfg: params.cfg, chatId, topicId }); if (typeof requireMention === "boolean") return requireMention; return resolveChannelGroupRequireMention({ cfg: params.cfg, channel: "telegram", groupId: chatId ?? params.groupId, accountId: params.accountId }); } function resolveWhatsAppGroupRequireMention(params) { return resolveChannelGroupRequireMention({ cfg: params.cfg, channel: "whatsapp", groupId: params.groupId, accountId: params.accountId }); } function resolveIMessageGroupRequireMention(params) { return resolveChannelGroupRequireMention({ cfg: params.cfg, channel: "imessage", groupId: params.groupId, accountId: params.accountId }); } function resolveDiscordGroupRequireMention(params) { const guildEntry = resolveDiscordGuildEntry(params.cfg.channels?.discord?.guilds, params.groupSpace); const channelEntries = guildEntry?.channels; if (channelEntries && Object.keys(channelEntries).length > 0) { const groupChannel = params.groupChannel; const channelSlug = normalizeDiscordSlug(groupChannel); const entry = (params.groupId ? channelEntries[params.groupId] : void 0) ?? (channelSlug ? channelEntries[channelSlug] ?? channelEntries[`#${channelSlug}`] : void 0) ?? (groupChannel ? channelEntries[normalizeDiscordSlug(groupChannel)] : void 0); if (entry && typeof entry.requireMention === "boolean") return entry.requireMention; } if (typeof guildEntry?.requireMention === "boolean") return guildEntry.requireMention; return true; } function resolveGoogleChatGroupRequireMention(params) { return resolveChannelGroupRequireMention({ cfg: params.cfg, channel: "googlechat", groupId: params.groupId, accountId: params.accountId }); } function resolveGoogleChatGroupToolPolicy(params) { return resolveChannelGroupToolsPolicy({ cfg: params.cfg, channel: "googlechat", groupId: params.groupId, accountId: params.accountId, senderId: params.senderId, senderName: params.senderName, senderUsername: params.senderUsername, senderE164: params.senderE164 }); } function resolveSlackGroupRequireMention(params) { const channels = resolveSlackAccount({ cfg: params.cfg, accountId: params.accountId }).channels ?? {}; if (Object.keys(channels).length === 0) return true; const channelId = params.groupId?.trim(); const channelName = params.groupChannel?.replace(/^#/, ""); const normalizedName = normalizeSlackSlug(channelName); const candidates = [ channelId ?? "", channelName ? `#${channelName}` : "", channelName ?? "", normalizedName ].filter(Boolean); let matched; for (const candidate of candidates) if (candidate && channels[candidate]) { matched = channels[candidate]; break; } const fallback = channels["*"]; const resolved = matched ?? fallback; if (typeof resolved?.requireMention === "boolean") return resolved.requireMention; return true; } function resolveTelegramGroupToolPolicy(params) { const { chatId } = parseTelegramGroupId(params.groupId); return resolveChannelGroupToolsPolicy({ cfg: params.cfg, channel: "telegram", groupId: chatId ?? params.groupId, accountId: params.accountId, senderId: params.senderId, senderName: params.senderName, senderUsername: params.senderUsername, senderE164: params.senderE164 }); } function resolveWhatsAppGroupToolPolicy(params) { return resolveChannelGroupToolsPolicy({ cfg: params.cfg, channel: "whatsapp", groupId: params.groupId, accountId: params.accountId, senderId: params.senderId, senderName: params.senderName, senderUsername: params.senderUsername, senderE164: params.senderE164 }); } function resolveIMessageGroupToolPolicy(params) { return resolveChannelGroupToolsPolicy({ cfg: params.cfg, channel: "imessage", groupId: params.groupId, accountId: params.accountId, senderId: params.senderId, senderName: params.senderName, senderUsername: params.senderUsername, senderE164: params.senderE164 }); } function resolveDiscordGroupToolPolicy(params) { const guildEntry = resolveDiscordGuildEntry(params.cfg.channels?.discord?.guilds, params.groupSpace); const channelEntries = guildEntry?.channels; if (channelEntries && Object.keys(channelEntries).length > 0) { const groupChannel = params.groupChannel; const channelSlug = normalizeDiscordSlug(groupChannel); const entry = (params.groupId ? channelEntries[params.groupId] : void 0) ?? (channelSlug ? channelEntries[channelSlug] ?? channelEntries[`#${channelSlug}`] : void 0) ?? (groupChannel ? channelEntries[normalizeDiscordSlug(groupChannel)] : void 0); const senderPolicy = resolveToolsBySender({ toolsBySender: entry?.toolsBySender, senderId: params.senderId, senderName: params.senderName, senderUsername: params.senderUsername, senderE164: params.senderE164 }); if (senderPolicy) return senderPolicy; if (entry?.tools) return entry.tools; } const guildSenderPolicy = resolveToolsBySender({ toolsBySender: guildEntry?.toolsBySender, senderId: params.senderId, senderName: params.senderName, senderUsername: params.senderUsername, senderE164: params.senderE164 }); if (guildSenderPolicy) return guildSenderPolicy; if (guildEntry?.tools) return guildEntry.tools; } function resolveSlackGroupToolPolicy(params) { const channels = resolveSlackAccount({ cfg: params.cfg, accountId: params.accountId }).channels ?? {}; if (Object.keys(channels).length === 0) return; const channelId = params.groupId?.trim(); const channelName = params.groupChannel?.replace(/^#/, ""); const normalizedName = normalizeSlackSlug(channelName); const candidates = [ channelId ?? "", channelName ? `#${channelName}` : "", channelName ?? "", normalizedName ].filter(Boolean); let matched; for (const candidate of candidates) if (candidate && channels[candidate]) { matched = channels[candidate]; break; } const resolved = matched ?? channels["*"]; const senderPolicy = resolveToolsBySender({ toolsBySender: resolved?.toolsBySender, senderId: params.senderId, senderName: params.senderName, senderUsername: params.senderUsername, senderE164: params.senderE164 }); if (senderPolicy) return senderPolicy; if (resolved?.tools) return resolved.tools; } //#endregion //#region src/channels/dock.ts const formatLower = (allowFrom) => allowFrom.map((entry) => String(entry).trim()).filter(Boolean).map((entry) => entry.toLowerCase()); const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const DOCKS = { telegram: { id: "telegram", capabilities: { chatTypes: [ "direct", "group", "channel", "thread" ], nativeCommands: true, blockStreaming: true }, outbound: { textChunkLimit: 4e3 }, config: { resolveAllowFrom: ({ cfg, accountId }) => (resolveTelegramAccount({ cfg, accountId }).config.allowFrom ?? []).map((entry) => String(entry)), formatAllowFrom: ({ allowFrom }) => allowFrom.map((entry) => String(entry).trim()).filter(Boolean).map((entry) => entry.replace(/^(telegram|tg):/i, "")).map((entry) => entry.toLowerCase()) }, groups: { resolveRequireMention: resolveTelegramGroupRequireMention, resolveToolPolicy: resolveTelegramGroupToolPolicy }, threading: { resolveReplyToMode: ({ cfg }) => cfg.channels?.telegram?.replyToMode ?? "first", buildToolContext: ({ context, hasRepliedRef }) => { const threadId = context.MessageThreadId ?? context.ReplyToId; return { currentChannelId: context.To?.trim() || void 0, currentThreadTs: threadId != null ? String(threadId) : void 0, hasRepliedRef }; } } }, whatsapp: { id: "whatsapp", capabilities: { chatTypes: ["direct", "group"], polls: true, reactions: true, media: true }, commands: { enforceOwnerForCommands: true, skipWhenConfigEmpty: true }, outbound: { textChunkLimit: 4e3 }, config: { resolveAllowFrom: ({ cfg, accountId }) => resolveWhatsAppAccount({ cfg, accountId }).allowFrom ?? [], formatAllowFrom: ({ allowFrom }) => allowFrom.map((entry) => String(entry).trim()).filter((entry) => Boolean(entry)).map((entry) => entry === "*" ? entry : normalizeWhatsAppTarget(entry)).filter((entry) => Boolean(entry)) }, groups: { resolveRequireMention: resolveWhatsAppGroupRequireMention, resolveToolPolicy: resolveWhatsAppGroupToolPolicy, resolveGroupIntroHint: () => "WhatsApp IDs: SenderId is the participant JID; [message_id: ...] is the message id for reactions (use SenderId as participant)." }, mentions: { stripPatterns: ({ ctx }) => { const selfE164 = (ctx.To ?? "").replace(/^whatsapp:/, ""); if (!selfE164) return []; const escaped = escapeRegExp(selfE164); return [escaped, `@${escaped}`]; } }, threading: { buildToolContext: ({ context, hasRepliedRef }) => { return { currentChannelId: context.From?.trim() || context.To?.trim() || void 0, currentThreadTs: context.ReplyToId, hasRepliedRef }; } } }, discord: { id: "discord", capabilities: { chatTypes: [ "direct", "channel", "thread" ], polls: true, reactions: true, media: true, nativeCommands: true, threads: true }, outbound: { textChunkLimit: 2e3 }, streaming: { blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1e3 } }, elevated: { allowFromFallback: ({ cfg }) => cfg.channels?.discord?.dm?.allowFrom }, config: { resolveAllowFrom: ({ cfg, accountId }) => (resolveDiscordAccount({ cfg, accountId }).config.dm?.allowFrom ?? []).map((entry) => String(entry)), formatAllowFrom: ({ allowFrom }) => formatLower(allowFrom) }, groups: { resolveRequireMention: resolveDiscordGroupRequireMention, resolveToolPolicy: resolveDiscordGroupToolPolicy }, mentions: { stripPatterns: () => ["<@!?\\d+>"] }, threading: { resolveReplyToMode: ({ cfg }) => cfg.channels?.discord?.replyToMode ?? "off", buildToolContext: ({ context, hasRepliedRef }) => ({ currentChannelId: context.To?.trim() || void 0, currentThreadTs: context.ReplyToId, hasRepliedRef }) } }, googlechat: { id: "googlechat", capabilities: { chatTypes: [ "direct", "group", "thread" ], reactions: true, media: true, threads: true, blockStreaming: true }, outbound: { textChunkLimit: 4e3 }, config: { resolveAllowFrom: ({ cfg, accountId }) => { const channel = cfg.channels?.googlechat; const normalized = normalizeAccountId$1(accountId); return ((channel?.accounts?.[normalized] ?? channel?.accounts?.[Object.keys(channel?.accounts ?? {}).find((key) => key.toLowerCase() === normalized.toLowerCase()) ?? ""])?.dm?.allowFrom ?? channel?.dm?.allowFrom ?? []).map((entry) => String(entry)); }, formatAllowFrom: ({ allowFrom }) => allowFrom.map((entry) => String(entry).trim()).filter(Boolean).map((entry) => entry.replace(/^(googlechat|google-chat|gchat):/i, "").replace(/^user:/i, "").replace(/^users\//i, "").toLowerCase()) }, groups: { resolveRequireMention: resolveGoogleChatGroupRequireMention, resolveToolPolicy: resolveGoogleChatGroupToolPolicy }, threading: { resolveReplyToMode: ({ cfg }) => cfg.channels?.googlechat?.replyToMode ?? "off", buildToolContext: ({ context, hasRepliedRef }) => { const threadId = context.MessageThreadId ?? context.ReplyToId; return { currentChannelId: context.To?.trim() || void 0, currentThreadTs: threadId != null ? String(threadId) : void 0, hasRepliedRef }; } } }, slack: { id: "slack", capabilities: { chatTypes: [ "direct", "channel", "thread" ], reactions: true, media: true, nativeCommands: true, threads: true }, outbound: { textChunkLimit: 4e3 }, streaming: { blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1e3 } }, config: { resolveAllowFrom: ({ cfg, accountId }) => (resolveSlackAccount({ cfg, accountId }).dm?.allowFrom ?? []).map((entry) => String(entry)), formatAllowFrom: ({ allowFrom }) => formatLower(allowFrom) }, groups: { resolveRequireMention: resolveSlackGroupRequireMention, resolveToolPolicy: resolveSlackGroupToolPolicy }, mentions: { stripPatterns: () => ["<@[^>]+>"] }, threading: { resolveReplyToMode: ({ cfg, accountId, chatType }) => resolveSlackReplyToMode(resolveSlackAccount({ cfg, accountId }), chatType), allowTagsWhenOff: true, buildToolContext: (params) => buildSlackThreadingToolContext(params) } }, signal: { id: "signal", capabilities: { chatTypes: ["direct", "group"], reactions: true, media: true }, outbound: { textChunkLimit: 4e3 }, streaming: { blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1e3 } }, config: { resolveAllowFrom: ({ cfg, accountId }) => (resolveSignalAccount({ cfg, accountId }).config.allowFrom ?? []).map((entry) => String(entry)), formatAllowFrom: ({ allowFrom }) => allowFrom.map((entry) => String(entry).trim()).filter(Boolean).map((entry) => entry === "*" ? "*" : normalizeE164(entry.replace(/^signal:/i, ""))).filter(Boolean) }, threading: { buildToolContext: ({ context, hasRepliedRef }) => { return { currentChannelId: (context.ChatType?.toLowerCase() === "direct" ? context.From ?? context.To : context.To)?.trim() || void 0, currentThreadTs: context.ReplyToId, hasRepliedRef }; } } }, imessage: { id: "imessage", capabilities: { chatTypes: ["direct", "group"], reactions: true, media: true }, outbound: { textChunkLimit: 4e3 }, config: { resolveAllowFrom: ({ cfg, accountId }) => (resolveIMessageAccount({ cfg, accountId }).config.allowFrom ?? []).map((entry) => String(entry)), formatAllowFrom: ({ allowFrom }) => allowFrom.map((entry) => String(entry).trim()).filter(Boolean) }, groups: { resolveRequireMention: resolveIMessageGroupRequireMention, resolveToolPolicy: resolveIMessageGroupToolPolicy }, threading: { buildToolContext: ({ context, hasRepliedRef }) => { return { currentChannelId: (context.ChatType?.toLowerCase() === "direct" ? context.From ?? context.To : context.To)?.trim() || void 0, currentThreadTs: context.ReplyToId, hasRepliedRef }; } } } }; function buildDockFromPlugin(plugin) { return { id: plugin.id, capabilities: plugin.capabilities, commands: plugin.commands, outbound: plugin.outbound?.textChunkLimit ? { textChunkLimit: plugin.outbound.textChunkLimit } : void 0, streaming: plugin.streaming ? { blockStreamingCoalesceDefaults: plugin.streaming.blockStreamingCoalesceDefaults } : void 0, elevated: plugin.elevated, config: plugin.config ? { resolveAllowFrom: plugin.config.resolveAllowFrom, formatAllowFrom: plugin.config.formatAllowFrom } : void 0, groups: plugin.groups, mentions: plugin.mentions, threading: plugin.threading, agentPrompt: plugin.agentPrompt }; } function listPluginDockEntries() { const registry = requireActivePluginRegistry(); const entries = []; const seen = /* @__PURE__ */ new Set(); for (const entry of registry.channels) { const plugin = entry.plugin; const id = String(plugin.id).trim(); if (!id || seen.has(id)) continue; seen.add(id); if (CHAT_CHANNEL_ORDER.includes(plugin.id)) continue; const dock = entry.dock ?? buildDockFromPlugin(plugin); entries.push({ id: plugin.id, dock, order: plugin.meta.order }); } return entries; } function listChannelDocks() { const baseEntries = CHAT_CHANNEL_ORDER.map((id) => ({ id, dock: DOCKS[id], order: getChatChannelMeta(id).order })); const pluginEntries = listPluginDockEntries(); const combined = [...baseEntries, ...pluginEntries]; combined.sort((a, b) => { const indexA = CHAT_CHANNEL_ORDER.indexOf(a.id); const indexB = CHAT_CHANNEL_ORDER.indexOf(b.id); const orderA = a.order ?? (indexA === -1 ? 999 : indexA); const orderB = b.order ?? (indexB === -1 ? 999 : indexB); if (orderA !== orderB) return orderA - orderB; return String(a.id).localeCompare(String(b.id)); }); return combined.map((entry) => entry.dock); } function getChannelDock(id) { const core = DOCKS[id]; if (core) return core; const pluginEntry = requireActivePluginRegistry().channels.find((entry) => entry.plugin.id === id); if (!pluginEntry) return; return pluginEntry.dock ?? buildDockFromPlugin(pluginEntry.plugin); } //#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_COMMON_IMAGE = "openclaw-sandbox-common: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/browser/bridge-server.ts async function startBrowserBridgeServer(params) { const host = params.host ?? "127.0.0.1"; const port = params.port ?? 0; const app = express(); app.use(express.json({ limit: "1mb" })); const authToken = params.authToken?.trim(); if (authToken) app.use((req, res, next) => { if (String(req.headers.authorization ?? "").trim() === `Bearer ${authToken}`) return next(); res.status(401).send("Unauthorized"); }); const state = { server: null, port, resolved: params.resolved, profiles: /* @__PURE__ */ new Map() }; registerBrowserRoutes(app, createBrowserRouteContext({ getState: () => state, onEnsureAttachTarget: params.onEnsureAttachTarget })); const server = await new Promise((resolve, reject) => { const s = app.listen(port, host, () => resolve(s)); s.once("error", reject); }); const resolvedPort = server.address()?.port ?? port; state.server = server; state.port = resolvedPort; state.resolved.controlPort = resolvedPort; return { server, port: resolvedPort, baseUrl: `http://${host}:${resolvedPort}`, state }; } async function stopBrowserBridgeServer(server) { await new Promise((resolve) => { server.close(() => resolve()); }); } //#endregion //#region src/agents/sandbox/browser-bridges.ts const BROWSER_BRIDGES = /* @__PURE__ */ new Map(); //#endregion //#region src/agents/sandbox/config-hash.ts function isPrimitive(value) { return value === null || typeof value !== "object" && typeof value !== "function"; } function normalizeForHash(value) { if (value === void 0) return; if (Array.isArray(value)) { const normalized = value.map(normalizeForHash).filter((item) => item !== void 0); const primitives = normalized.filter(isPrimitive); if (primitives.length === normalized.length) return [...primitives].toSorted((a, b) => primitiveToString(a).localeCompare(primitiveToString(b))); return normalized; } if (value && typeof value === "object") { const entries = Object.entries(value).toSorted(([a], [b]) => a.localeCompare(b)); const normalized = {}; for (const [key, entryValue] of entries) { const next = normalizeForHash(entryValue); if (next !== void 0) normalized[key] = next; } return normalized; } return value; } function primitiveToString(value) { if (value === null) return "null"; if (typeof value === "string") return value; if (typeof value === "number") return String(value); if (typeof value === "boolean") return value ? "true" : "false"; return JSON.stringify(value); } function computeSandboxConfigHash(input) { const payload = normalizeForHash(input); const raw = JSON.stringify(payload); return crypto.createHash("sha1").update(raw).digest("hex"); } //#endregion //#region src/agents/sandbox/registry.ts async function readRegistry() { try { const raw = await fs$1.readFile(SANDBOX_REGISTRY_PATH, "utf-8"); const parsed = JSON.parse(raw); if (parsed && Array.isArray(parsed.entries)) return parsed; } catch {} return { entries: [] }; } async function writeRegistry(registry) { await fs$1.mkdir(SANDBOX_STATE_DIR, { recursive: true }); await fs$1.writeFile(SANDBOX_REGISTRY_PATH, `${JSON.stringify(registry, null, 2)}\n`, "utf-8"); } async function updateRegistry(entry) { const registry = await readRegistry(); const existing = registry.entries.find((item) => item.containerName === entry.containerName); const next = registry.entries.filter((item) => item.containerName !== entry.containerName); next.push({ ...entry, createdAtMs: existing?.createdAtMs ?? entry.createdAtMs, image: existing?.image ?? entry.image, configHash: entry.configHash ?? existing?.configHash }); await writeRegistry({ entries: next }); } async function removeRegistryEntry(containerName) { const registry = await readRegistry(); const next = registry.entries.filter((item) => item.containerName !== containerName); if (next.length === registry.entries.length) return; await writeRegistry({ entries: next }); } async function readBrowserRegistry() { try { const raw = await fs$1.readFile(SANDBOX_BROWSER_REGISTRY_PATH, "utf-8"); const parsed = JSON.parse(raw); if (parsed && Array.isArray(parsed.entries)) return parsed; } catch {} return { entries: [] }; } async function writeBrowserRegistry(registry) { await fs$1.mkdir(SANDBOX_STATE_DIR, { recursive: true }); await fs$1.writeFile(SANDBOX_BROWSER_REGISTRY_PATH, `${JSON.stringify(registry, null, 2)}\n`, "utf-8"); } async function updateBrowserRegistry(entry) { const registry = await readBrowserRegistry(); const existing = registry.entries.find((item) => item.containerName === entry.containerName); const next = registry.entries.filter((item) => item.containerName !== entry.containerName); next.push({ ...entry, createdAtMs: existing?.createdAtMs ?? entry.createdAtMs, image: existing?.image ?? entry.image }); await writeBrowserRegistry({ entries: next }); } async function removeBrowserRegistryEntry(containerName) { const registry = await readBrowserRegistry(); const next = registry.entries.filter((item) => item.containerName !== containerName); if (next.length === registry.entries.length) return; await writeBrowserRegistry({ entries: next }); } //#endregion //#region src/agents/sandbox/shared.ts function slugifySessionKey(value) { const trimmed = value.trim() || "session"; const hash = crypto.createHash("sha1").update(trimmed).digest("hex").slice(0, 8); return `${trimmed.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 32) || "session"}-${hash}`; } function resolveSandboxWorkspaceDir(root, sessionKey) { const resolvedRoot = resolveUserPath(root); const slug = slugifySessionKey(sessionKey); return path.join(resolvedRoot, slug); } function resolveSandboxScopeKey(scope, sessionKey) { const trimmed = sessionKey.trim() || "main"; if (scope === "shared") return "shared"; if (scope === "session") return trimmed; return `agent:${resolveAgentIdFromSessionKey(trimmed)}`; } function resolveSandboxAgentId(scopeKey) { const trimmed = scopeKey.trim(); if (!trimmed || trimmed === "shared") return; const parts = trimmed.split(":").filter(Boolean); if (parts[0] === "agent" && parts[1]) return normalizeAgentId(parts[1]); return resolveAgentIdFromSessionKey(trimmed); } //#endregion //#region src/agents/sandbox/docker.ts const HOT_CONTAINER_WINDOW_MS = 300 * 1e3; function execDocker(args, opts) { return new Promise((resolve, reject) => { const child = spawn("docker", args, { stdio: [ "ignore", "pipe", "pipe" ] }); let stdou