UNPKG

@gguf/claw

Version:

WhatsApp gateway CLI (Baileys web) with Pi RPC agent

1,320 lines (1,302 loc) 108 kB
import { r as STATE_DIR } from "./paths-scjhy7N2.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-Dm2EOhrH.js"; import { c as defaultRuntime, m as CHAT_CHANNEL_ORDER, p as CHANNEL_IDS, v as getChatChannelMeta, w as requireActivePluginRegistry } from "./subsystem-CAq3uyo7.js"; import { d as normalizeE164, h as resolveUserPath } from "./utils-CKSrBNwq.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-CMs5Y7l-.js"; import { t as formatCliCommand } from "./command-format-ChfKqObn.js"; import { i as loadConfig } from "./config-CAuZ-EkU.js"; import { a as normalizeWhatsAppTarget, b as normalizeChatType, c as resolveTelegramAccount, m as resolveSlackReplyToMode, p as resolveSlackAccount, r as normalizeChannelId, v as resolveDiscordAccount } from "./plugins-BYIWo0Cp.js"; import { M as DEFAULT_OPENCLAW_BROWSER_COLOR, P as DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME, j as DEFAULT_BROWSER_EVALUATE_ENABLED } from "./chrome-BNSd7Bie.js"; import { o as syncSkillsToWorkspace } from "./skills-D5JDj3TR.js"; import { t as registerBrowserRoutes } from "./routes-DchZU3EK.js"; import { a as resolveProfile, t as createBrowserRouteContext } from "./server-context-vChIAqjH.js"; import { c as listDeliverableMessageChannels, l as normalizeMessageChannel } from "./message-channel-Bpfe5l5f.js"; import { n as resolveWhatsAppAccount } from "./accounts-BgZmhIm6.js"; import { r as resolveSessionTranscriptPath, t as resolveDefaultSessionStorePath } from "./paths-Bb0nwPeu.js"; import { t as emitSessionTranscriptUpdate } from "./transcript-events-ChU6IQwp.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 crypto from "node:crypto"; import { CURRENT_SESSION_VERSION, SessionManager } from "@mariozechner/pi-coding-agent"; import express from "express"; //#region src/channels/conversation-label.ts function extractConversationId(from) { const trimmed = from?.trim(); if (!trimmed) return; const parts = trimmed.split(":").filter(Boolean); return parts.length > 0 ? parts[parts.length - 1] : trimmed; } function shouldAppendId(id) { if (/^[0-9]+$/.test(id)) return true; if (id.includes("@g.us")) return true; return false; } function resolveConversationLabel(ctx) { const explicit = ctx.ConversationLabel?.trim(); if (explicit) return explicit; const threadLabel = ctx.ThreadLabel?.trim(); if (threadLabel) return threadLabel; if (normalizeChatType(ctx.ChatType) === "direct") return ctx.SenderName?.trim() || ctx.From?.trim() || void 0; const base = ctx.GroupChannel?.trim() || ctx.GroupSubject?.trim() || ctx.GroupSpace?.trim() || ctx.From?.trim() || ""; if (!base) return; const id = extractConversationId(ctx.From); if (!id) return base; if (!shouldAppendId(id)) return base; if (base === id) return base; if (base.includes(id)) return base; if (base.toLowerCase().includes(" id:")) return base; if (base.startsWith("#") || base.startsWith("@")) return base; return `${base} id:${id}`; } //#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 stdout = ""; let stderr = ""; child.stdout?.on("data", (chunk) => { stdout += chunk.toString(); }); child.stderr?.on("data", (chunk) => { stderr += chunk.toString(); }); child.on("close", (code) => { const exitCode = code ?? 0; if (exitCode !== 0 && !opts?.allowFailure) { reject(new Error(stderr.trim() || `docker ${args.join(" ")} failed`)); return; } resolve({ stdout, stderr, code: exitCode }); }); }); } async function readDockerPort(containerName, port) { const result = await execDocker([ "port", containerName, `${port}/tcp` ], { allowFailure: true }); if (result.code !== 0) return null; const match = (result.stdout.trim().split(/\r?\n/)[0] ?? "").match(/:(\d+)\s*$/); if (!match) return null; const mapped = Number.parseInt(match[1] ?? "", 10); return Number.isFinite(mapped) ? mapped : null; } async function dockerImageExists(image) { const result = await execDocker([ "image", "inspect", image ], { allowFailure: true }); if (result.code === 0) return true; const stderr = result.stderr.trim(); if (stderr.includes("No such image")) return false; throw new Error(`Failed to inspect sandbox image: ${stderr}`); } async function ensureDockerImage(image) { if (await dockerImageExists(image)) return; if (image === DEFAULT_SANDBOX_IMAGE) { await execDocker(["pull", "debian:bookworm-slim"]); await execDocker([ "tag", "debian:bookworm-slim", DEFAULT_SANDBOX_IMAGE ]); return; } throw new Error(`Sandbox image not found: ${image}. Build or pull it first.`); } async function dockerContainerState(name) { const result = await execDocker([ "inspect", "-f", "{{.State.Running}}", name ], { allowFailure: true }); if (result.code !== 0) return { exists: false, running: false }; return { exists: true, running: result.stdout.trim() === "true" }; } function normalizeDockerLimit(value) { if (value === void 0 || value === null) return; if (typeof value === "number") return Number.isFinite(value) ? String(value) : void 0; const trimmed = value.trim(); return trimmed ? trimmed : void 0; } function formatUlimitValue(name, value) { if (!name.trim()) return null; if (typeof value === "number" || typeof value === "string") { const raw = String(value).trim(); return raw ? `${name}=${raw}` : null; } const soft = typeof value.soft === "number" ? Math.max(0, value.soft) : void 0; const hard = typeof value.hard === "number" ? Math.max(0, value.hard) : void 0; if (soft === void 0 && hard === void 0) return null; if (soft === void 0) return `${name}=${hard}`; if (hard === void 0) return `${name}=${soft}`; return `${name}=${soft}:${hard}`; } function buildSandboxCreateArgs(params) { const createdAtMs = params.createdAtMs ?? Date.now(); const args = [ "create", "--name", params.name ]; args.push("--label", "openclaw.sandbox=1"); args.push("--label", `openclaw.sessionKey=${params.scopeKey}`); args.push("--label", `openclaw.createdAtMs=${createdAtMs}`); if (params.configHash) args.push("--label", `openclaw.configHash=${params.configHash}`); for (const [key, value] of Object.entries(params.labels ?? {})) if (key && value) args.push("--label", `${key}=${value}`); if (params.cfg.readOnlyRoot) args.push("--read-only"); for (const entry of params.cfg.tmpfs) args.push("--tmpfs", entry); if (params.cfg.network) args.push("--network", params.cfg.network); if (params.cfg.user) args.push("--user", params.cfg.user); for (const cap of params.cfg.capDrop) args.push("--cap-drop", cap); args.push("--security-opt", "no-new-privileges"); if (params.cfg.seccompProfile) args.push("--security-opt", `seccomp=${params.cfg.seccompProfile}`); if (params.cfg.apparmorProfile) args.push("--security-opt", `apparmor=${params.cfg.apparmorProfile}`); for (const entry of params.cfg.dns ?? []) if (entry.trim()) args.push("--dns", entry); for (const entry of params.cfg.extraHosts ?? []) if (entry.trim()) args.push("--add-host", entry); if (typeof params.cfg.pidsLimit === "number" && params.cfg.pidsLimit > 0) args.push("--pids-limit", String(params.cfg.pidsLimit)); const memory = normalizeDockerLimit(params.cfg.memory); if (memory) args.push("--memory", memory); const memorySwap = normalizeDockerLimit(params.cfg.memorySwap); if (memorySwap) args.push("--memory-swap", memorySwap); if (typeof params.cfg.cpus === "number" && params.cfg.cpus > 0) args.push("--cpus", String(params.cfg.cpus)); for (const [name, value] of Object.entries(params.cfg.ulimits ?? {})) { const formatted = formatUlimitValue(name, value); if (formatted) args.push("--ulimit", formatted); } if (params.cfg.binds?.length) for (const bind of params.cfg.binds) args.push("-v", bind); return args; } async function createSandboxContainer(params) { const { name, cfg, workspaceDir, scopeKey } = params; await ensureDockerImage(cfg.image); const args = buildSandboxCreateArgs({ name, cfg, scopeKey, configHash: params.configHash }); args.push("--workdir", cfg.workdir); const mainMountSuffix = params.workspaceAccess === "ro" && workspaceDir === params.agentWorkspaceDir ? ":ro" : ""; args.push("-v", `${workspaceDir}:${cfg.workdir}${mainMountSuffix}`); if (params.workspaceAccess !== "none" && workspaceDir !== params.agentWorkspaceDir) { const agentMountSuffix = params.workspaceAccess === "ro" ? ":ro" : ""; args.push("-v", `${params.agentWorkspaceDir}:${SANDBOX_AGENT_WORKSPACE_MOUNT}${agentMountSuffix}`); } args.push(cfg.image, "sleep", "infinity"); await execDocker(args); await execDocker(["start", name]); if (cfg.setupCommand?.trim()) await execDocker([ "exec", "-i", name, "sh", "-lc", cfg.setupCommand ]); } async function readContainerConfigHash(containerName) { const readLabel = async (label) => { const result = await execDocker([ "inspect", "-f", `{{ index .Config.Labels "${label}" }}`, containerName ], { allowFailure: true }); if (result.code !== 0) return null; const raw = result.stdout.trim(); if (!raw || raw === "<no value>") return null; return raw; }; return await readLabel("openclaw.configHash"); } function formatSandboxRecreateHint(params) { if (params.scope === "session") return formatCliCommand(`openclaw sandbox recreate --session ${params.sessionKey}`); if (params.scope === "agent") return formatCliCommand(`openclaw sandbox recreate --agent ${resolveSandboxAgentId(params.sessionKey) ?? "main"}`); return formatCliCommand("openclaw sandbox recreate --all"); } async function ensureSandboxContainer(params) { const scopeKey = resolveSandboxScopeKey(params.cfg.scope, params.sessionKey); const slug = params.cfg.scope === "shared" ? "shared" : slugifySessionKey(scopeKey); const containerName = `${params.cfg.docker.containerPrefix}${slug}`.slice(0, 63); const expectedHash = computeSandboxConfigHash({ docker: params.cfg.docker, workspaceAccess: params.cfg.workspaceAccess, workspaceDir: params.workspaceDir, agentWorkspaceDir: params.agentWorkspaceDir }); const now = Date.now(); const state = await dockerContainerState(containerName); let hasContainer = state.exists; let running = state.running; let currentHash = null; let hashMismatch = false; let registryEntry; if (hasContainer) { registryEntry = (await readRegistry()).entries.find((entry) => entry.containerName === containerName); currentHash = await readContainerConfigHash(containerName); if (!currentHash) currentHash = registryEntry?.configHash ?? null; hashMismatch = !currentHash || currentHash !== expectedHash; if (hashMismatch) { const lastUsedAtMs = registryEntry?.lastUsedAtMs; if (running && (typeof lastUsedAtMs !== "number" || now - lastUsedAtMs < HOT_CONTAINER_WINDOW_MS)) { const hint = formatSandboxRecreateHint({ scope: params.cfg.scope, sessionKey: scopeKey }); defaultRuntime.log(`Sandbox config changed for ${containerName} (recently used). Recreate to apply: ${hint}`); } else { await execDocker([ "rm", "-f", containerName ], { allowFailure: true }); hasContainer = false; running = false; } } } if (!hasContainer) await createSandboxContainer({ name: containerName, cfg: params.cfg.docker, workspaceDir: params.workspaceDir, workspaceAccess: params.cfg.workspaceAccess, agentWorkspaceDir: params.agentWorkspaceDir, scopeKey, configHash: expectedHash }); else if (!running) await execDocker(["start", containerName]); await updateRegistry({ containerName, sessionKey: scopeKey, createdAtMs: now, lastUsedAtMs: now, image: params.cfg.docker.image, configHash: hashMismatch && running ? currentHash ?? void 0 : expectedHash }); return containerName; } //#endregion //#region src/agents/sandbox/browser.ts async function waitForSandboxCdp(params) { const deadline = Date.now() + Math.max(0, params.timeoutMs); const url = `http://127.0.0.1:${params.cdpPort}/json/version`; while (Date.now() < deadline) { try { const ctrl = new AbortController(); const t = setTimeout(() => ctrl.abort(), 1e3); try { if ((await fetch(url, { signal: ctrl.signal })).ok) return true; } finally { clearTimeout(t); } } catch {} await new Promise((r) => setTimeout(r, 150)); } return false; } function buildSandboxBrowserResolvedConfig(params) { return { enabled: true, evaluateEnabled: params.evaluateEnabled, controlPort: params.controlPort, cdpProtocol: "http", cdpHost: "127.0.0.1", cdpIsLoopback: true, remoteCdpTimeoutMs: 1500, remoteCdpHandshakeTimeoutMs: 3e3, color: DEFAULT_OPENCLAW_BROWSER_COLOR, executablePath: void 0, headless: params.headless, noSandbox: false, attachOnly: true, defaultProfile: DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME, profiles: { [DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME]: { cdpPort: params.cdpPort, color: DEFAULT_OPENCLAW_BROWSER_COLOR } } }; } async function ensureSandboxBrowserImage(image) { if ((await execDocker([ "image", "inspect", image ], { allowFailure: true })).code === 0) return; throw new Error(`Sandbox browser image not found: ${image}. Build it with scripts/sandbox-browser-setup.sh.`); } async function ensureSandboxBrowser(params) { if (!params.cfg.browser.enabled) return null; if (!isToolAllowed(params.cfg.tools, "browser")) return null; const slug = params.cfg.scope === "shared" ? "shared" : slugifySessionKey(params.scopeKey); const containerName = `${params.cfg.browser.containerPrefix}${slug}`.slice(0, 63); const state = await dockerContainerState(containerName); if (!state.exists) { await ensureSandboxBrowserImage(params.cfg.browser.image ?? DEFAULT_SANDBOX_BROWSER_IMAGE); const args = buildSandboxCreateArgs({ name: containerName, cfg: params.cfg.docker, scopeKey: params.scopeKey, labels: { "openclaw.sandboxBrowser": "1" } }); const mainMountSuffix = params.cfg.workspaceAccess === "ro" && params.workspaceDir === params.agentWorkspaceDir ? ":ro" : ""; args.push("-v", `${params.workspaceDir}:${params.cfg.docker.workdir}${mainMountSuffix}`); if (params.cfg.workspaceAccess !== "none" && params.workspaceDir !== params.agentWorkspaceDir) { const agentMountSuffix = params.cfg.workspaceAccess === "ro" ? ":ro" : ""; args.push("-v", `${params.agentWorkspaceDir}:${SANDBOX_AGENT_WORKSPACE_MOUNT}${agentMountSuffix}`); } args.push("-p", `127.0.0.1::${params.cfg.browser.cdpPort}`); if (params.cfg.browser.enableNoVnc && !params.cfg.browser.headless) args.push("-p", `127.0.0.1::${params.cfg.browser.noVncPort}`); args.push("-e", `OPENCLAW_BROWSER_HEADLESS=${params.cfg.browser.headless ? "1" : "0"}`); args.push("-e", `OPENCLAW_BROWSER_ENABLE_NOVNC=${params.cfg.browser.enableNoVnc ? "1" : "0"}`); args.push("-e", `OPENCLAW_BROWSER_CDP_PORT=${params.cfg.browser.cdpPort}`); args.push("-e", `OPENCLAW_BROWSER_VNC_PORT=${params.cfg.browser.vncPort}`); args.push("-e", `OPENCLAW_BROWSER_NOVNC_PORT=${params.cfg.browser.noVncPort}`); args.push(params.cfg.browser.image); await execDocker(args); await execDocker(["start", containerName]); } else if (!state.running) await execDocker(["start", containerName]); const mappedCdp = await readDockerPort(containerName, params.cfg.browser.cdpPort); if (!mappedCdp) throw new Error(`Failed to resolve CDP port mapping for ${containerName}.`); const mappedNoVnc = params.cfg.browser.enableNoVnc && !params.cfg.browser.headless ? await readDockerPort(containerName, params.cfg.browser.noVncPort) : null; const existing = BROWSER_BRIDGES.get(params.scopeKey); const existingProfile = existing ? resolveProfile(existing.bridge.state.resolved, DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME) : null; const shouldReuse = existing && existing.containerName === containerName && existingProfile?.cdpPort === mappedCdp; if (existing && !shouldReuse) { await stopBrowserBridgeServer(existing.bridge.server).catch(() => void 0); BROWSER_BRIDGES.delete(params.scopeKey); } const bridge = (() => { if (shouldReuse && existing) return existing.bridge; return null; })(); const ensureBridge = async () => { if (bridge) return bridge; const onEnsureAttachTarget = params.cfg.browser.autoStart ? async () => { const state = await dockerContainerState(containerName); if (state.exists && !state.running) await execDocker(["start", containerName]); if (!await waitForSandboxCdp({ cdpPort: mappedCdp, timeoutMs: params.cfg.browser.autoStartTimeoutMs })) throw new Error(`Sandbox browser CDP did not become reachable on 127.0.0.1:${mappedCdp} within ${params.cfg.browser.autoStartTimeoutMs}ms.`); } : void 0; return await startBrowserBridgeServer({ resolved: buildSandboxBrowserResolvedConfig({ controlPort: 0, cdpPort: mappedCdp, headless: params.cfg.browser.headless, evaluateEnabled: params.evaluateEnabled ?? DEFAULT_BROWSER_EVALUATE_ENABLED }), onEnsureAttachTarget }); }; const resolvedBridge = await ensureBridge(); if (!shouldReuse) BROWSER_BRIDGES.set(params.scopeKey, { bridge: resolvedBridge, containerName }); const now = Date.now(); await updateBrowserRegistry({ containerName, sessionKey: params.scopeKey, createdAtMs: now, lastUsedAtMs: now, image: params.cfg.browser.image, cdpPort: mappedCdp, noVncPort: mappedNoVnc ?? void 0 }); const noVncUrl = mappedNoVnc && params.cfg.browser.enableNoVnc && !params.cfg.browser.headless ? `http://127.0.0.1:${mappedNoVnc}/vnc.html?autoconnect=1&resize=remote` : void 0; return { bridgeUrl: resolvedBridge.baseUrl, noVncUrl, containerName }; } //#endregion //#region src/agents/sandbox/prune.ts let lastPruneAtMs = 0; async function pruneSandboxContainers(cfg) { const now = Date.now(); const idleHours = cfg.prune.idleHours; const maxAgeDays = cfg.prune.maxAgeDays; if (idleHours === 0 && maxAgeDays === 0) return; const registry = await readRegistry(); for (const entry of registry.entries) { const idleMs = now - entry.lastUsedAtMs; const ageMs = now - entry.createdAtMs; if (idleHours > 0 && idleMs > idleHours * 60 * 60 * 1e3 || maxAgeDays > 0 && ageMs > maxAgeDays * 24 * 60 * 60 * 1e3) try { await execDocker([ "rm", "-f", entry.containerName ], { allowFailure: true }); } catch {} finally { await removeRegistryEntry(entry.containerName); } } } async function pruneSandboxBrowsers(cfg) { const now = Date.now(); const idleHours = cfg.prune.idleHours; const maxAgeDays = cfg.prune.maxAgeDays; if (idleHours === 0 && maxAgeDays === 0) return; const registry = await readBrowserRegistry(); for (const entry of registry.entries) { const idleMs = now - entry.lastUsedAtMs; const ageMs = now - entry.createdAtMs; if (idleHours > 0 && idleMs > idleHours * 60 * 60 * 1e3 || maxAgeDays > 0 && ageMs > maxAgeDays * 24 * 60 * 60 * 1e3) try { await execDocker([ "rm", "-f", entry.containerName ], { allowFailure: true }); } catch {} finally { await removeBrowserRegistryEntry(entry.containerName); const bridge = BROWSER_BRIDGES.get(entry.sessionKey); if (bridge?.containerName === entry.containerName) { await stopBrowserBridgeServer(bridge.bridge.server).catch(() => void 0); BROWSER_BRIDGES.delete(entry.sessionKey); } } } } async function maybePruneSandboxes(cfg) { const now = Date.now(); if (now - lastPruneAtMs < 300 * 1e3) return; lastPruneAtMs = now; try { await pruneSandboxContainers(cfg); await pruneSandboxBrowsers(cfg); } catch (error) { const message = error instanceof Error ? error.message : typeof error === "string" ? error : JSON.stringify(error); defaultRuntime.error?.(`Sandbox prune failed: ${message ?? "unknown error"}`); } } //#endregion //#region src/config/sessions/group.ts const getGroupSurfaces = () => new Set([...listDeliverableMessageChannels(), "webchat"]); function normalizeGroupLabel(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 shortenGroupId(value) { const trimmed = value?.trim() ?? ""; if (!trimmed) return ""; if (trimmed.length <= 14) return trimmed; return `${trimmed.slice(0, 6)}...${trimmed.slice(-4)}`; } function buildGroupDisplayName(params) { const providerKey = (params.provider?.trim().toLowerCase() || "group").trim(); const groupChannel = params.groupChannel?.trim(); const space = params.space?.trim(); const subject = params.subject?.trim(); const detail = (groupChannel && space ? `${space}${groupChannel.startsWith("#") ? "" : "#"}${groupChannel}` : groupChannel || subject || space || "") || ""; const fallbackId = params.id?.trim() || params.key; const rawLabel = detail || fallbackId; let token = normalizeGroupLabel(rawLabel); if (!token) token = normalizeGroupLabel(shortenGroupId(rawLabel)); if (!params.groupChannel && token.startsWith("#")) token = token.replace(/^#+/, ""); if (token && !/^[@#]/.test(token) && !token.startsWith("g-") && !token.includes("#")) token = `g-${token}`; return token ? `${providerKey}:${token}` : providerKey; } function resolveGroupSessionKey(ctx) { const from = typeof ctx.From === "string" ? ctx.From.trim() : ""; const chatType = ctx.ChatType?.trim().toLowerCase(); const normalizedChatType = chatType === "channel" ? "channel" : chatType === "group" ? "group" : void 0; const isWhatsAppGroupId = from.toLowerCase().endsWith("@g.us"); if (!(normalizedChatType === "group" || normalizedChatType === "channel" || from.includes(":group:") || from.includes(":channel:") || isWhatsAppGroupId)) return null; const providerHint = ctx.Provider?.trim().toLowerCase(); const parts = from.split(":").filter(Boolean); const head = parts[0]?.trim().toLowerCase() ?? ""; const headIsSurface = head ? getGroupSurfaces().has(head) : false; const provider = headIsSurface ? head : providerHint ?? (isWhatsAppGroupId ? "whatsapp" : void 0); if (!provider) return null; const second = parts[1]?.trim().toLowerCase(); const secondIsKind = second === "group" || second === "channel"; const kind = secondIsKind ? second : from.includes(":channel:") || normalizedChatType === "channel" ? "channel" : "group"; const finalId = (headIsSurface ? secondIsKind ? parts.slice(2).join(":") : parts.slice(1).join(":") : from).trim().toLowerCase(); if (!finalId) return null; return { key: `${provider}:${kind}:${finalId}`, channel: provider, id: finalId, chatType: kind === "channel" ? "channel" : "group" }; } //#endregion //#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 { allowlis