diff --git a/CHANGELOG.md b/CHANGELOG.md index 59b246352..6be30c218 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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). diff --git a/docs/gateway/configuration.md b/docs/gateway/configuration.md index 9d3899e56..33e0983a3 100644 --- a/docs/gateway/configuration.md +++ b/docs/gateway/configuration.md @@ -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` diff --git a/docs/gateway/sandbox-vs-tool-policy-vs-elevated.md b/docs/gateway/sandbox-vs-tool-policy-vs-elevated.md index 49c1334dc..f5fbf7420 100644 --- a/docs/gateway/sandbox-vs-tool-policy-vs-elevated.md +++ b/docs/gateway/sandbox-vs-tool-policy-vs-elevated.md @@ -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`. diff --git a/docs/multi-agent-sandbox-tools.md b/docs/multi-agent-sandbox-tools.md index e11c9a07b..2431f824c 100644 --- a/docs/multi-agent-sandbox-tools.md +++ b/docs/multi-agent-sandbox-tools.md @@ -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). diff --git a/src/agents/sandbox-explain.test.ts b/src/agents/sandbox-explain.test.ts index 27bbd7a57..d2d99f946 100644 --- a/src/agents/sandbox-explain.test.ts +++ b/src/agents/sandbox-explain.test.ts @@ -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: { diff --git a/src/agents/sandbox.ts b/src/agents/sandbox.ts index 58f1c6e60..62bba4ff3 100644 --- a/src/agents/sandbox.ts +++ b/src/agents/sandbox.ts @@ -246,10 +246,53 @@ function normalizeToolList(values?: string[]) { .map((value) => value.toLowerCase()); } +const TOOL_GROUPS: Record = { + // 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,