import type { ClawdbotConfig } from "../config/config.js"; import { loadConfig } from "../config/config.js"; import { loadSessionStore, resolveAgentMainSessionKey, resolveMainSessionKey, resolveStorePath, } from "../config/sessions.js"; import { buildAgentMainSessionKey, normalizeAgentId, normalizeMainKey, parseAgentSessionKey, resolveAgentIdFromSessionKey, } from "../routing/session-key.js"; import type { RuntimeEnv } from "../runtime.js"; import { resolveAgentConfig } from "../agents/agent-scope.js"; import { resolveSandboxConfigForAgent, resolveSandboxToolPolicyForAgent, } from "../agents/sandbox.js"; type SandboxExplainOptions = { session?: string; agent?: string; json: boolean; }; const SANDBOX_DOCS_URL = "https://docs.clawd.bot/sandbox"; const KNOWN_PROVIDER_KEYS = new Set([ "whatsapp", "telegram", "discord", "slack", "signal", "imessage", "webchat", ]); function normalizeExplainSessionKey(params: { cfg: ClawdbotConfig; agentId: string; session?: string; }): string { const raw = (params.session ?? "").trim(); if (!raw) { return resolveAgentMainSessionKey({ cfg: params.cfg, agentId: params.agentId, }); } if (raw.includes(":")) return raw; if (raw === "global") return "global"; return buildAgentMainSessionKey({ agentId: params.agentId, mainKey: normalizeMainKey(raw), }); } function inferProviderFromSessionKey(params: { cfg: ClawdbotConfig; sessionKey: string; }): string | undefined { const parsed = parseAgentSessionKey(params.sessionKey); if (!parsed) return undefined; const rest = parsed.rest.trim(); if (!rest) return undefined; const parts = rest.split(":").filter(Boolean); if (parts.length === 0) return undefined; const configuredMainKey = normalizeMainKey(params.cfg.session?.mainKey); if (parts[0] === configuredMainKey) return undefined; const candidate = parts[0]?.trim().toLowerCase(); return candidate && KNOWN_PROVIDER_KEYS.has(candidate) ? candidate : undefined; } function resolveActiveProvider(params: { cfg: ClawdbotConfig; agentId: string; sessionKey: string; }): string | undefined { const storePath = resolveStorePath(params.cfg.session?.store, { agentId: params.agentId, }); const store = loadSessionStore(storePath); const entry = store[params.sessionKey]; const candidate = ( entry?.lastProvider ?? entry?.providerOverride ?? entry?.provider ?? "" ) .trim() .toLowerCase(); if (candidate && KNOWN_PROVIDER_KEYS.has(candidate)) return candidate; return inferProviderFromSessionKey({ cfg: params.cfg, sessionKey: params.sessionKey, }); } function resolveElevatedAllowListForProvider(params: { provider: string; allowFrom?: Record | undefined>; discordFallback?: Array; }): Array | undefined { switch (params.provider) { case "whatsapp": return params.allowFrom?.whatsapp; case "telegram": return params.allowFrom?.telegram; case "discord": { const hasExplicit = Boolean( params.allowFrom && Object.hasOwn(params.allowFrom, "discord"), ); if (hasExplicit) return params.allowFrom?.discord; return params.discordFallback; } case "slack": return params.allowFrom?.slack; case "signal": return params.allowFrom?.signal; case "imessage": return params.allowFrom?.imessage; case "webchat": return params.allowFrom?.webchat; default: return undefined; } } export async function sandboxExplainCommand( opts: SandboxExplainOptions, runtime: RuntimeEnv, ): Promise { const cfg = loadConfig(); const defaultAgentId = resolveAgentIdFromSessionKey(resolveMainSessionKey(cfg)); const resolvedAgentId = normalizeAgentId( opts.agent?.trim() ? opts.agent : opts.session?.trim() ? resolveAgentIdFromSessionKey(opts.session) : defaultAgentId, ); const sessionKey = normalizeExplainSessionKey({ cfg, agentId: resolvedAgentId, session: opts.session, }); const sandboxCfg = resolveSandboxConfigForAgent(cfg, resolvedAgentId); const toolPolicy = resolveSandboxToolPolicyForAgent(cfg, resolvedAgentId); const mainSessionKey = resolveAgentMainSessionKey({ cfg, agentId: resolvedAgentId, }); const sessionIsSandboxed = sandboxCfg.mode === "all" ? true : sandboxCfg.mode === "off" ? false : sessionKey.trim() !== mainSessionKey.trim(); const provider = resolveActiveProvider({ cfg, agentId: resolvedAgentId, sessionKey, }); const agentConfig = resolveAgentConfig(cfg, resolvedAgentId); const elevatedGlobal = cfg.tools?.elevated; const elevatedAgent = agentConfig?.tools?.elevated; const elevatedGlobalEnabled = elevatedGlobal?.enabled !== false; const elevatedAgentEnabled = elevatedAgent?.enabled !== false; const elevatedEnabled = elevatedGlobalEnabled && elevatedAgentEnabled; const discordFallback = provider === "discord" ? cfg.discord?.dm?.allowFrom : undefined; const globalAllow = provider ? resolveElevatedAllowListForProvider({ provider, allowFrom: elevatedGlobal?.allowFrom as unknown as Record< string, Array | undefined >, discordFallback, }) : undefined; const agentAllow = provider ? resolveElevatedAllowListForProvider({ provider, allowFrom: elevatedAgent?.allowFrom as unknown as Record< string, Array | undefined >, }) : undefined; const allowTokens = (values?: Array) => (values ?? []).map((v) => String(v).trim()).filter(Boolean); const globalAllowTokens = allowTokens(globalAllow); const agentAllowTokens = allowTokens(agentAllow); const elevatedAllowedByConfig = elevatedEnabled && Boolean(provider) && globalAllowTokens.length > 0 && (elevatedAgent?.allowFrom ? agentAllowTokens.length > 0 : true); const elevatedAlwaysAllowedByConfig = elevatedAllowedByConfig && globalAllowTokens.includes("*") && (elevatedAgent?.allowFrom ? agentAllowTokens.includes("*") : true); const elevatedFailures: Array<{ gate: string; key: string }> = []; if (!elevatedGlobalEnabled) { elevatedFailures.push({ gate: "enabled", key: "tools.elevated.enabled" }); } if (!elevatedAgentEnabled) { elevatedFailures.push({ gate: "enabled", key: "agents.list[].tools.elevated.enabled", }); } if (provider && globalAllowTokens.length === 0) { elevatedFailures.push({ gate: "allowFrom", key: provider === "discord" && discordFallback ? "tools.elevated.allowFrom.discord (or discord.dm.allowFrom fallback)" : `tools.elevated.allowFrom.${provider}`, }); } if (provider && elevatedAgent?.allowFrom && agentAllowTokens.length === 0) { elevatedFailures.push({ gate: "allowFrom", key: `agents.list[].tools.elevated.allowFrom.${provider}`, }); } const fixIt: string[] = []; if (sandboxCfg.mode !== "off") { fixIt.push("agents.defaults.sandbox.mode=off"); fixIt.push("agents.list[].sandbox.mode=off"); } fixIt.push("tools.sandbox.tools.allow"); fixIt.push("tools.sandbox.tools.deny"); fixIt.push("agents.list[].tools.sandbox.tools.allow"); fixIt.push("agents.list[].tools.sandbox.tools.deny"); fixIt.push("tools.elevated.enabled"); if (provider) fixIt.push(`tools.elevated.allowFrom.${provider}`); const payload = { docsUrl: SANDBOX_DOCS_URL, agentId: resolvedAgentId, sessionKey, mainSessionKey, sandbox: { mode: sandboxCfg.mode, scope: sandboxCfg.scope, perSession: sandboxCfg.scope === "session", workspaceAccess: sandboxCfg.workspaceAccess, workspaceRoot: sandboxCfg.workspaceRoot, sessionIsSandboxed, tools: { allow: toolPolicy.allow, deny: toolPolicy.deny, sources: toolPolicy.sources, }, }, elevated: { enabled: elevatedEnabled, provider, allowedByConfig: elevatedAllowedByConfig, alwaysAllowedByConfig: elevatedAlwaysAllowedByConfig, allowFrom: { global: provider ? globalAllowTokens : undefined, agent: elevatedAgent?.allowFrom && provider ? agentAllowTokens : undefined, }, failures: elevatedFailures, }, fixIt, } as const; if (opts.json) { runtime.log(`${JSON.stringify(payload, null, 2)}\n`); return; } const lines: string[] = []; lines.push("Effective sandbox:"); lines.push(` agentId: ${payload.agentId}`); lines.push(` sessionKey: ${payload.sessionKey}`); lines.push(` mainSessionKey: ${payload.mainSessionKey}`); lines.push( ` runtime: ${payload.sandbox.sessionIsSandboxed ? "sandboxed" : "direct"}`, ); lines.push( ` mode=${payload.sandbox.mode} scope=${payload.sandbox.scope} perSession=${payload.sandbox.perSession}`, ); lines.push( ` workspaceAccess=${payload.sandbox.workspaceAccess} workspaceRoot=${payload.sandbox.workspaceRoot}`, ); lines.push(""); lines.push("Sandbox tool policy:"); lines.push( ` allow (${payload.sandbox.tools.sources.allow.source}): ${payload.sandbox.tools.allow.join(", ") || "(empty)"}`, ); lines.push( ` deny (${payload.sandbox.tools.sources.deny.source}): ${payload.sandbox.tools.deny.join(", ") || "(empty)"}`, ); lines.push(""); lines.push("Elevated:"); lines.push(` enabled: ${payload.elevated.enabled}`); lines.push(` provider: ${payload.elevated.provider ?? "(unknown)"}`); lines.push(` allowedByConfig: ${payload.elevated.allowedByConfig}`); if (payload.elevated.failures.length > 0) { lines.push( ` failing gates: ${payload.elevated.failures .map((f) => `${f.gate} (${f.key})`) .join(", ")}`, ); } if (payload.sandbox.mode === "non-main" && payload.sandbox.sessionIsSandboxed) { lines.push(""); lines.push( `Hint: sandbox mode is non-main; use main session key to run direct: ${payload.mainSessionKey}`, ); } lines.push(""); lines.push("Fix-it:"); for (const key of payload.fixIt) lines.push(` - ${key}`); lines.push(""); lines.push(`Docs: ${payload.docsUrl}`); runtime.log(`${lines.join("\n")}\n`); }