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

@@ -5,6 +5,7 @@
### Changes ### Changes
- Subagents: add config to set default sub-agent model (`agents.defaults.subagents.model` + per-agent override); still overridden by `sessions_spawn.model`. - Subagents: add config to set default sub-agent model (`agents.defaults.subagents.model` + per-agent override); still overridden by `sessions_spawn.model`.
- Plugins: restore full voice-call plugin parity (Telnyx/Twilio, streaming, inbound policies, tools/CLI). - Plugins: restore full voice-call plugin parity (Telnyx/Twilio, streaming, inbound policies, tools/CLI).
- Sandbox: support tool-policy groups in `tools.sandbox.tools` (e.g. `group:memory`, `group:fs`) to reduce config churn.
### Fixes ### Fixes
- Tools/Models: MiniMax vision now uses the Coding Plan VLM endpoint (`/v1/coding_plan/vlm`) so the `image` tool works with MiniMax keys (also accepts `@/path/to/file.png`-style inputs). - Tools/Models: MiniMax vision now uses the Coding Plan VLM endpoint (`/v1/coding_plan/vlm`) so the `image` tool works with MiniMax keys (also accepts `@/path/to/file.png`-style inputs).

View File

@@ -1564,6 +1564,7 @@ Defaults (if enabled):
- auto-prune: idle > 24h OR age > 7d - auto-prune: idle > 24h OR age > 7d
- tool policy: allow only `exec`, `process`, `read`, `write`, `edit`, `apply_patch`, `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`, `session_status` (deny wins) - tool policy: allow only `exec`, `process`, `read`, `write`, `edit`, `apply_patch`, `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`, `session_status` (deny wins)
- configure via `tools.sandbox.tools`, override per-agent via `agents.list[].tools.sandbox.tools` - configure via `tools.sandbox.tools`, override per-agent via `agents.list[].tools.sandbox.tools`
- tool group shorthands supported in sandbox policy: `group:runtime`, `group:fs`, `group:sessions`, `group:memory` (see [Sandbox vs Tool Policy vs Elevated](/gateway/sandbox-vs-tool-policy-vs-elevated#tool-groups-shorthands))
- optional sandboxed browser (Chromium + CDP, noVNC observer) - optional sandboxed browser (Chromium + CDP, noVNC observer)
- hardening knobs: `network`, `user`, `pidsLimit`, `memory`, `cpus`, `ulimits`, `seccompProfile`, `apparmorProfile` - hardening knobs: `network`, `user`, `pidsLimit`, `memory`, `cpus`, `ulimits`, `seccompProfile`, `apparmorProfile`

View File

@@ -49,6 +49,30 @@ Rules of thumb:
- `deny` always wins. - `deny` always wins.
- If `allow` is non-empty, everything else is treated as blocked. - If `allow` is non-empty, everything else is treated as blocked.
### Tool groups (shorthands)
For sandbox tool policy, you can use `group:*` entries that expand to multiple tools:
```json5
{
tools: {
sandbox: {
tools: {
allow: ["group:runtime", "group:fs", "group:sessions", "group:memory"]
}
}
}
}
```
Available groups:
- `group:runtime`: `exec`, `bash`, `process`
- `group:fs`: `read`, `write`, `edit`, `apply_patch`
- `group:sessions`: `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`, `session_status`
- `group:memory`: `memory_search`, `memory_get`
Legacy shorthand: `memory` expands to `group:memory`.
## Elevated: exec-only “run on host” ## Elevated: exec-only “run on host”
Elevated does **not** grant extra tools; it only affects `exec`. Elevated does **not** grant extra tools; it only affects `exec`.

View File

@@ -173,6 +173,17 @@ The filtering order is:
Each level can further restrict tools, but cannot grant back denied tools from earlier levels. Each level can further restrict tools, but cannot grant back denied tools from earlier levels.
If `agents.list[].tools.sandbox.tools` is set, it replaces `tools.sandbox.tools` for that agent. If `agents.list[].tools.sandbox.tools` is set, it replaces `tools.sandbox.tools` for that agent.
### Tool groups (shorthands)
Sandbox tool policy supports `group:*` entries that expand to multiple concrete tools:
- `group:runtime`: `exec`, `bash`, `process`
- `group:fs`: `read`, `write`, `edit`, `apply_patch`
- `group:sessions`: `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`, `session_status`
- `group:memory`: `memory_search`, `memory_get`
Legacy shorthand: `memory` expands to `group:memory`.
### Elevated Mode ### Elevated Mode
`tools.elevated` is the global baseline (sender-based allowlist). `agents.list[].tools.elevated` can further restrict elevated for specific agents (both must allow). `tools.elevated` is the global baseline (sender-based allowlist). `agents.list[].tools.elevated` can further restrict elevated for specific agents (both must allow).

View File

@@ -35,6 +35,59 @@ describe("sandbox explain helpers", () => {
expect(policy.sources.deny.source).toBe("global"); 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", () => { it("includes config key paths + main-session hint for non-main mode", () => {
const cfg: ClawdbotConfig = { const cfg: ClawdbotConfig = {
agents: { agents: {

View File

@@ -246,10 +246,53 @@ function normalizeToolList(values?: string[]) {
.map((value) => value.toLowerCase()); .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) { 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; if (deny.has(name.toLowerCase())) return false;
const allow = normalizeToolList(policy.allow); const allow = normalizeToolList(expandToolGroups(policy.allow));
if (allow.length === 0) return true; if (allow.length === 0) return true;
return allow.includes(name.toLowerCase()); return allow.includes(name.toLowerCase());
} }
@@ -480,21 +523,27 @@ export function resolveSandboxToolPolicyForAgent(
: Array.isArray(globalDeny) : Array.isArray(globalDeny)
? globalDeny ? globalDeny
: DEFAULT_TOOL_DENY; : DEFAULT_TOOL_DENY;
let allow = Array.isArray(agentAllow) const allow = Array.isArray(agentAllow)
? agentAllow ? agentAllow
: Array.isArray(globalAllow) : Array.isArray(globalAllow)
? globalAllow ? globalAllow
: DEFAULT_TOOL_ALLOW; : DEFAULT_TOOL_ALLOW;
const expandedDeny = expandToolGroups(deny);
let expandedAllow = expandToolGroups(allow);
// `image` is essential for multimodal workflows; always include it in sandboxed // `image` is essential for multimodal workflows; always include it in sandboxed
// sessions unless explicitly denied. // sessions unless explicitly denied.
if (!deny.includes("image") && !allow.includes("image")) { if (
allow = [...allow, "image"]; !expandedDeny.map((v) => v.toLowerCase()).includes("image") &&
!expandedAllow.map((v) => v.toLowerCase()).includes("image")
) {
expandedAllow = [...expandedAllow, "image"];
} }
return { return {
allow, allow: expandedAllow,
deny, deny: expandedDeny,
sources: { sources: {
allow: allowSource, allow: allowSource,
deny: denySource, deny: denySource,