feat(sandbox): add sandbox explain inspector

This commit is contained in:
Peter Steinberger
2026-01-10 20:28:34 +01:00
parent 4533dd6e5d
commit 9f9098406c
20 changed files with 951 additions and 56 deletions

View File

@@ -19,12 +19,17 @@ import {
loadConfig,
STATE_DIR_CLAWDBOT,
} from "../config/config.js";
import { normalizeAgentId, normalizeMainKey } from "../routing/session-key.js";
import {
buildAgentMainSessionKey,
normalizeAgentId,
normalizeMainKey,
} from "../routing/session-key.js";
import { defaultRuntime } from "../runtime.js";
import { resolveUserPath } from "../utils.js";
import {
resolveAgentConfig,
resolveAgentIdFromSessionKey,
resolveSessionAgentId,
} from "./agent-scope.js";
import { syncSkillsToWorkspace } from "./skills.js";
import {
@@ -44,6 +49,24 @@ export type SandboxToolPolicy = {
deny?: string[];
};
export type SandboxToolPolicySource = {
source: "agent" | "global" | "default";
/**
* Config key path hint for humans.
* (Arrays use `agents.list[].…` form.)
*/
key: string;
};
export type SandboxToolPolicyResolved = {
allow: string[];
deny: string[];
sources: {
allow: SandboxToolPolicySource;
deny: SandboxToolPolicySource;
};
};
export type SandboxWorkspaceAccess = "none" | "ro" | "rw";
export type SandboxBrowserConfig = {
@@ -377,6 +400,65 @@ function resolveSandboxAgentId(scopeKey: string): string | undefined {
return resolveAgentIdFromSessionKey(trimmed);
}
export function resolveSandboxToolPolicyForAgent(
cfg?: ClawdbotConfig,
agentId?: string,
): SandboxToolPolicyResolved {
const agentConfig =
cfg && agentId ? resolveAgentConfig(cfg, agentId) : undefined;
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",
} satisfies SandboxToolPolicySource)
: Array.isArray(globalAllow)
? ({
source: "global",
key: "tools.sandbox.tools.allow",
} satisfies SandboxToolPolicySource)
: ({
source: "default",
key: "tools.sandbox.tools.allow",
} satisfies SandboxToolPolicySource);
const denySource = Array.isArray(agentDeny)
? ({
source: "agent",
key: "agents.list[].tools.sandbox.tools.deny",
} satisfies SandboxToolPolicySource)
: Array.isArray(globalDeny)
? ({
source: "global",
key: "tools.sandbox.tools.deny",
} satisfies SandboxToolPolicySource)
: ({
source: "default",
key: "tools.sandbox.tools.deny",
} satisfies SandboxToolPolicySource);
return {
allow: Array.isArray(agentAllow)
? agentAllow
: Array.isArray(globalAllow)
? globalAllow
: DEFAULT_TOOL_ALLOW,
deny: Array.isArray(agentDeny)
? agentDeny
: Array.isArray(globalDeny)
? globalDeny
: DEFAULT_TOOL_DENY,
sources: {
allow: allowSource,
deny: denySource,
},
};
}
export function resolveSandboxConfigForAgent(
cfg?: ClawdbotConfig,
agentId?: string,
@@ -396,6 +478,8 @@ export function resolveSandboxConfigForAgent(
perSession: agentSandbox?.perSession ?? agent?.perSession,
});
const toolPolicy = resolveSandboxToolPolicyForAgent(cfg, agentId);
return {
mode: agentSandbox?.mode ?? agent?.mode ?? "off",
scope,
@@ -416,14 +500,8 @@ export function resolveSandboxConfigForAgent(
agentBrowser: agentSandbox?.browser,
}),
tools: {
allow:
agentConfig?.tools?.sandbox?.tools?.allow ??
cfg?.tools?.sandbox?.tools?.allow ??
DEFAULT_TOOL_ALLOW,
deny:
agentConfig?.tools?.sandbox?.tools?.deny ??
cfg?.tools?.sandbox?.tools?.deny ??
DEFAULT_TOOL_DENY,
allow: toolPolicy.allow,
deny: toolPolicy.deny,
},
prune: resolveSandboxPruneConfig({
scope,
@@ -443,6 +521,92 @@ function shouldSandboxSession(
return sessionKey.trim() !== mainKey.trim();
}
export function resolveSandboxRuntimeStatus(params: {
cfg?: ClawdbotConfig;
sessionKey?: string;
}): {
agentId: string;
sessionKey: string;
mainSessionKey: string;
mode: SandboxConfig["mode"];
sandboxed: boolean;
toolPolicy: SandboxToolPolicyResolved;
} {
const sessionKey = params.sessionKey?.trim() ?? "";
const agentId = resolveSessionAgentId({
sessionKey,
config: params.cfg,
});
const cfg = params.cfg;
const sandboxCfg = resolveSandboxConfigForAgent(cfg, agentId);
const mainSessionKey = buildAgentMainSessionKey({
agentId,
mainKey: normalizeMainKey(cfg?.session?.mainKey),
});
const sandboxed = sessionKey
? shouldSandboxSession(sandboxCfg, sessionKey, mainSessionKey)
: false;
return {
agentId,
sessionKey,
mainSessionKey,
mode: sandboxCfg.mode,
sandboxed,
toolPolicy: resolveSandboxToolPolicyForAgent(cfg, agentId),
};
}
export function formatSandboxToolPolicyBlockedMessage(params: {
cfg?: ClawdbotConfig;
sessionKey?: string;
toolName: string;
}): string | undefined {
const tool = params.toolName.trim().toLowerCase();
if (!tool) return undefined;
const runtime = resolveSandboxRuntimeStatus({
cfg: params.cfg,
sessionKey: params.sessionKey,
});
if (!runtime.sandboxed) return undefined;
const deny = new Set(normalizeToolList(runtime.toolPolicy.deny));
const allow = normalizeToolList(runtime.toolPolicy.allow);
const allowSet = allow.length > 0 ? new Set(allow) : null;
const blockedByDeny = deny.has(tool);
const blockedByAllow = allowSet ? !allowSet.has(tool) : false;
if (!blockedByDeny && !blockedByAllow) return undefined;
const reasons: string[] = [];
const fixes: string[] = [];
if (blockedByDeny) {
reasons.push("deny list");
fixes.push(`Remove "${tool}" from ${runtime.toolPolicy.sources.deny.key}.`);
}
if (blockedByAllow) {
reasons.push("allow list");
fixes.push(
`Add "${tool}" to ${runtime.toolPolicy.sources.allow.key} (or set it to [] to allow all).`,
);
}
const lines: string[] = [];
lines.push(
`Tool "${tool}" blocked by sandbox tool policy (mode=${runtime.mode}).`,
);
lines.push(`Session: ${runtime.sessionKey || "(unknown)"}`);
lines.push(`Reason: ${reasons.join(" + ")}`);
lines.push("Fix:");
lines.push(`- agents.defaults.sandbox.mode=off (disable sandbox)`);
for (const fix of fixes) lines.push(`- ${fix}`);
if (runtime.mode === "non-main") {
lines.push(`- Use main session key (direct): ${runtime.mainSessionKey}`);
}
lines.push(`- See: clawdbot sandbox explain --session ${runtime.sessionKey}`);
return lines.join("\n");
}
function slugifySessionKey(value: string) {
const trimmed = value.trim() || "session";
const hash = crypto