@gguf/claw
Version:
WhatsApp gateway CLI (Baileys web) with Pi RPC agent
1,397 lines (1,381 loc) • 108 kB
JavaScript
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