feat(sandbox): add sandbox explain inspector
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user