feat(sandbox): add tool-policy groups
This commit is contained in:
@@ -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).
|
||||
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user