import { resolveAgentConfig } from "../agents/agent-scope.js"; import { resolveSandboxConfigForAgent, resolveSandboxToolPolicyForAgent, } from "../agents/sandbox.js"; 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 { formatDocsLink } from "../terminal/links.js"; import { colorize, isRich, theme } from "../terminal/theme.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 rich = isRich(); const heading = (value: string) => colorize(rich, theme.heading, value); const key = (value: string) => colorize(rich, theme.muted, value); const value = (val: string) => colorize(rich, theme.info, val); const ok = (val: string) => colorize(rich, theme.success, val); const warn = (val: string) => colorize(rich, theme.warn, val); const err = (val: string) => colorize(rich, theme.error, val); const bool = (flag: boolean) => (flag ? ok("true") : err("false")); const lines: string[] = []; lines.push(heading("Effective sandbox:")); lines.push(` ${key("agentId:")} ${value(payload.agentId)}`); lines.push(` ${key("sessionKey:")} ${value(payload.sessionKey)}`); lines.push(` ${key("mainSessionKey:")} ${value(payload.mainSessionKey)}`); lines.push( ` ${key("runtime:")} ${ payload.sandbox.sessionIsSandboxed ? warn("sandboxed") : ok("direct") }`, ); lines.push( ` ${key("mode:")} ${value(payload.sandbox.mode)} ${key("scope:")} ${value( payload.sandbox.scope, )} ${key("perSession:")} ${bool(payload.sandbox.perSession)}`, ); lines.push( ` ${key("workspaceAccess:")} ${value( payload.sandbox.workspaceAccess, )} ${key("workspaceRoot:")} ${value(payload.sandbox.workspaceRoot)}`, ); lines.push(""); lines.push(heading("Sandbox tool policy:")); lines.push( ` ${key(`allow (${payload.sandbox.tools.sources.allow.source}):`)} ${value( payload.sandbox.tools.allow.join(", ") || "(empty)", )}`, ); lines.push( ` ${key(`deny (${payload.sandbox.tools.sources.deny.source}):`)} ${value( payload.sandbox.tools.deny.join(", ") || "(empty)", )}`, ); lines.push(""); lines.push(heading("Elevated:")); lines.push(` ${key("enabled:")} ${bool(payload.elevated.enabled)}`); lines.push( ` ${key("provider:")} ${value(payload.elevated.provider ?? "(unknown)")}`, ); lines.push( ` ${key("allowedByConfig:")} ${bool(payload.elevated.allowedByConfig)}`, ); if (payload.elevated.failures.length > 0) { lines.push( ` ${key("failing gates:")} ${warn( payload.elevated.failures.map((f) => `${f.gate} (${f.key})`).join(", "), )}`, ); } if ( payload.sandbox.mode === "non-main" && payload.sandbox.sessionIsSandboxed ) { lines.push(""); lines.push( `${warn("Hint:")} sandbox mode is non-main; use main session key to run direct: ${value( payload.mainSessionKey, )}`, ); } lines.push(""); lines.push(heading("Fix-it:")); for (const key of payload.fixIt) lines.push(` - ${key}`); lines.push(""); lines.push( `${key("Docs:")} ${formatDocsLink("/sandbox", "docs.clawd.bot/sandbox")}`, ); runtime.log(`${lines.join("\n")}\n`); }