feat(sandbox): add tool-policy groups

This commit is contained in:
Peter Steinberger
2026-01-12 21:51:26 +00:00
parent 26d5cca97c
commit 2faf7cea93
6 changed files with 146 additions and 7 deletions

View File

@@ -35,6 +35,59 @@ describe("sandbox explain helpers", () => {
expect(policy.sources.deny.source).toBe("global");
});
it("expands group tool shorthands inside sandbox tool policy", () => {
const cfg: ClawdbotConfig = {
agents: {
defaults: {
sandbox: { mode: "all", scope: "agent" },
},
list: [
{
id: "work",
workspace: "~/clawd-work",
tools: {
sandbox: { tools: { allow: ["group:memory", "group:fs"] } },
},
},
],
},
};
const policy = resolveSandboxToolPolicyForAgent(cfg, "work");
expect(policy.allow).toEqual([
"memory_search",
"memory_get",
"read",
"write",
"edit",
"apply_patch",
"image",
]);
});
it("supports legacy 'memory' shorthand and deny wins after expansion", () => {
const cfg: ClawdbotConfig = {
agents: {
defaults: {
sandbox: { mode: "all", scope: "agent" },
},
},
tools: {
sandbox: {
tools: {
allow: ["memory"],
deny: ["memory_get"],
},
},
},
};
const policy = resolveSandboxToolPolicyForAgent(cfg, "main");
expect(policy.allow).toContain("memory_search");
expect(policy.allow).toContain("memory_get");
expect(policy.deny).toContain("memory_get");
});
it("includes config key paths + main-session hint for non-main mode", () => {
const cfg: ClawdbotConfig = {
agents: {

View File

@@ -246,10 +246,53 @@ function normalizeToolList(values?: string[]) {
.map((value) => value.toLowerCase());
}
const TOOL_GROUPS: Record<string, string[]> = {
// NOTE: Keep canonical (lowercase) tool names here.
"group:memory": ["memory_search", "memory_get"],
// Basic workspace/file tools
"group:fs": ["read", "write", "edit", "apply_patch"],
// Session management tools
"group:sessions": [
"sessions_list",
"sessions_history",
"sessions_send",
"sessions_spawn",
"session_status",
],
// Host/runtime execution tools
"group:runtime": ["exec", "bash", "process"],
};
function expandToolGroupEntry(entry: string): string[] {
const raw = entry.trim();
if (!raw) return [];
const lower = raw.toLowerCase();
// Back-compat shorthand: "memory" => "group:memory"
if (lower === "memory") return TOOL_GROUPS["group:memory"];
const group = TOOL_GROUPS[lower];
if (group) return group;
return [raw];
}
function expandToolGroups(values?: string[]): string[] {
if (!values) return [];
const out: string[] = [];
for (const value of values) {
for (const expanded of expandToolGroupEntry(value)) {
const trimmed = expanded.trim();
if (!trimmed) continue;
out.push(trimmed);
}
}
return out;
}
function isToolAllowed(policy: SandboxToolPolicy, name: string) {
const deny = new Set(normalizeToolList(policy.deny));
const deny = new Set(normalizeToolList(expandToolGroups(policy.deny)));
if (deny.has(name.toLowerCase())) return false;
const allow = normalizeToolList(policy.allow);
const allow = normalizeToolList(expandToolGroups(policy.allow));
if (allow.length === 0) return true;
return allow.includes(name.toLowerCase());
}
@@ -480,21 +523,27 @@ export function resolveSandboxToolPolicyForAgent(
: Array.isArray(globalDeny)
? globalDeny
: DEFAULT_TOOL_DENY;
let allow = Array.isArray(agentAllow)
const allow = Array.isArray(agentAllow)
? agentAllow
: Array.isArray(globalAllow)
? globalAllow
: DEFAULT_TOOL_ALLOW;
const expandedDeny = expandToolGroups(deny);
let expandedAllow = expandToolGroups(allow);
// `image` is essential for multimodal workflows; always include it in sandboxed
// sessions unless explicitly denied.
if (!deny.includes("image") && !allow.includes("image")) {
allow = [...allow, "image"];
if (
!expandedDeny.map((v) => v.toLowerCase()).includes("image") &&
!expandedAllow.map((v) => v.toLowerCase()).includes("image")
) {
expandedAllow = [...expandedAllow, "image"];
}
return {
allow,
deny,
allow: expandedAllow,
deny: expandedDeny,
sources: {
allow: allowSource,
deny: denySource,