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
- 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).
- Sandbox: support tool-policy groups in `tools.sandbox.tools` (e.g. `group:memory`, `group:fs`) to reduce config churn.
### 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).

View File

@@ -1564,6 +1564,7 @@ Defaults (if enabled):
- 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)
- 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)
- hardening knobs: `network`, `user`, `pidsLimit`, `memory`, `cpus`, `ulimits`, `seccompProfile`, `apparmorProfile`

View File

@@ -49,6 +49,30 @@ Rules of thumb:
- `deny` always wins.
- 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 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.
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
`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");
});
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,