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