fix: honor wildcard tool allowlists
This commit is contained in:
@@ -1970,6 +1970,7 @@ Example (provider/model-specific allowlist):
|
|||||||
```
|
```
|
||||||
|
|
||||||
`tools.allow` / `tools.deny` configure a global tool allow/deny policy (deny wins).
|
`tools.allow` / `tools.deny` configure a global tool allow/deny policy (deny wins).
|
||||||
|
Matching is case-insensitive and supports `*` wildcards (`"*"` means all tools).
|
||||||
This is applied even when the Docker sandbox is **off**.
|
This is applied even when the Docker sandbox is **off**.
|
||||||
|
|
||||||
Example (disable browser/canvas everywhere):
|
Example (disable browser/canvas everywhere):
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ You can globally allow/deny tools via `tools.allow` / `tools.deny` in `clawdbot.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Matching is case-insensitive.
|
||||||
|
- `*` wildcards are supported (`"*"` means all tools).
|
||||||
|
|
||||||
## Tool profiles (base allowlist)
|
## Tool profiles (base allowlist)
|
||||||
|
|
||||||
`tools.profile` sets a **base tool allowlist** before `tools.allow`/`tools.deny`.
|
`tools.profile` sets a **base tool allowlist** before `tools.allow`/`tools.deny`.
|
||||||
|
|||||||
36
src/agents/pi-tools.policy.test.ts
Normal file
36
src/agents/pi-tools.policy.test.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import type { AgentTool, AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { filterToolsByPolicy, isToolAllowedByPolicyName } from "./pi-tools.policy.js";
|
||||||
|
|
||||||
|
function createStubTool(name: string): AgentTool<unknown, unknown> {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
label: name,
|
||||||
|
description: "",
|
||||||
|
parameters: {},
|
||||||
|
execute: async () => ({}) as AgentToolResult<unknown>,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("pi-tools.policy", () => {
|
||||||
|
it("treats * in allow as allow-all", () => {
|
||||||
|
const tools = [createStubTool("read"), createStubTool("exec")];
|
||||||
|
const filtered = filterToolsByPolicy(tools, { allow: ["*"] });
|
||||||
|
expect(filtered.map((tool) => tool.name)).toEqual(["read", "exec"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("treats * in deny as deny-all", () => {
|
||||||
|
const tools = [createStubTool("read"), createStubTool("exec")];
|
||||||
|
const filtered = filterToolsByPolicy(tools, { deny: ["*"] });
|
||||||
|
expect(filtered).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("supports wildcard allow/deny patterns", () => {
|
||||||
|
expect(isToolAllowedByPolicyName("web_fetch", { allow: ["web_*"] })).toBe(true);
|
||||||
|
expect(isToolAllowedByPolicyName("web_search", { deny: ["web_*"] })).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps apply_patch when exec is allowlisted", () => {
|
||||||
|
expect(isToolAllowedByPolicyName("apply_patch", { allow: ["exec"] })).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,6 +4,52 @@ import type { AnyAgentTool } from "./pi-tools.types.js";
|
|||||||
import type { SandboxToolPolicy } from "./sandbox.js";
|
import type { SandboxToolPolicy } from "./sandbox.js";
|
||||||
import { expandToolGroups, normalizeToolName } from "./tool-policy.js";
|
import { expandToolGroups, normalizeToolName } from "./tool-policy.js";
|
||||||
|
|
||||||
|
type CompiledPattern =
|
||||||
|
| { kind: "all" }
|
||||||
|
| { kind: "exact"; value: string }
|
||||||
|
| { kind: "regex"; value: RegExp };
|
||||||
|
|
||||||
|
function compilePattern(pattern: string): CompiledPattern {
|
||||||
|
const normalized = normalizeToolName(pattern);
|
||||||
|
if (!normalized) return { kind: "exact", value: "" };
|
||||||
|
if (normalized === "*") return { kind: "all" };
|
||||||
|
if (!normalized.includes("*")) return { kind: "exact", value: normalized };
|
||||||
|
const escaped = normalized.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
return {
|
||||||
|
kind: "regex",
|
||||||
|
value: new RegExp(`^${escaped.replaceAll("\\*", ".*")}$`),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function compilePatterns(patterns?: string[]): CompiledPattern[] {
|
||||||
|
if (!Array.isArray(patterns)) return [];
|
||||||
|
return expandToolGroups(patterns)
|
||||||
|
.map(compilePattern)
|
||||||
|
.filter((pattern) => pattern.kind !== "exact" || pattern.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesAny(name: string, patterns: CompiledPattern[]): boolean {
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
if (pattern.kind === "all") return true;
|
||||||
|
if (pattern.kind === "exact" && name === pattern.value) return true;
|
||||||
|
if (pattern.kind === "regex" && pattern.value.test(name)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeToolPolicyMatcher(policy: SandboxToolPolicy) {
|
||||||
|
const deny = compilePatterns(policy.deny);
|
||||||
|
const allow = compilePatterns(policy.allow);
|
||||||
|
return (name: string) => {
|
||||||
|
const normalized = normalizeToolName(name);
|
||||||
|
if (matchesAny(normalized, deny)) return false;
|
||||||
|
if (allow.length === 0) return true;
|
||||||
|
if (matchesAny(normalized, allow)) return true;
|
||||||
|
if (normalized === "apply_patch" && matchesAny("exec", allow)) return true;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const DEFAULT_SUBAGENT_TOOL_DENY = [
|
const DEFAULT_SUBAGENT_TOOL_DENY = [
|
||||||
// Session management - main agent orchestrates
|
// Session management - main agent orchestrates
|
||||||
"sessions_list",
|
"sessions_list",
|
||||||
@@ -35,22 +81,13 @@ export function resolveSubagentToolPolicy(cfg?: ClawdbotConfig): SandboxToolPoli
|
|||||||
|
|
||||||
export function isToolAllowedByPolicyName(name: string, policy?: SandboxToolPolicy): boolean {
|
export function isToolAllowedByPolicyName(name: string, policy?: SandboxToolPolicy): boolean {
|
||||||
if (!policy) return true;
|
if (!policy) return true;
|
||||||
const deny = new Set(expandToolGroups(policy.deny));
|
return makeToolPolicyMatcher(policy)(name);
|
||||||
const allowRaw = expandToolGroups(policy.allow);
|
|
||||||
const allow = allowRaw.length > 0 ? new Set(allowRaw) : null;
|
|
||||||
const normalized = normalizeToolName(name);
|
|
||||||
if (deny.has(normalized)) return false;
|
|
||||||
if (allow) {
|
|
||||||
if (allow.has(normalized)) return true;
|
|
||||||
if (normalized === "apply_patch" && allow.has("exec")) return true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function filterToolsByPolicy(tools: AnyAgentTool[], policy?: SandboxToolPolicy) {
|
export function filterToolsByPolicy(tools: AnyAgentTool[], policy?: SandboxToolPolicy) {
|
||||||
if (!policy) return tools;
|
if (!policy) return tools;
|
||||||
return tools.filter((tool) => isToolAllowedByPolicyName(tool.name, policy));
|
const matcher = makeToolPolicyMatcher(policy);
|
||||||
|
return tools.filter((tool) => matcher(tool.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
type ToolPolicyConfig = {
|
type ToolPolicyConfig = {
|
||||||
|
|||||||
21
src/agents/sandbox/tool-policy.test.ts
Normal file
21
src/agents/sandbox/tool-policy.test.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import type { SandboxToolPolicy } from "./types.js";
|
||||||
|
import { isToolAllowed } from "./tool-policy.js";
|
||||||
|
|
||||||
|
describe("sandbox tool policy", () => {
|
||||||
|
it("allows all tools with * allow", () => {
|
||||||
|
const policy: SandboxToolPolicy = { allow: ["*"], deny: [] };
|
||||||
|
expect(isToolAllowed(policy, "browser")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("denies all tools with * deny", () => {
|
||||||
|
const policy: SandboxToolPolicy = { allow: [], deny: ["*"] };
|
||||||
|
expect(isToolAllowed(policy, "read")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("supports wildcard patterns", () => {
|
||||||
|
const policy: SandboxToolPolicy = { allow: ["web_*"] };
|
||||||
|
expect(isToolAllowed(policy, "web_fetch")).toBe(true);
|
||||||
|
expect(isToolAllowed(policy, "read")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -8,12 +8,46 @@ import type {
|
|||||||
SandboxToolPolicySource,
|
SandboxToolPolicySource,
|
||||||
} from "./types.js";
|
} from "./types.js";
|
||||||
|
|
||||||
|
type CompiledPattern =
|
||||||
|
| { kind: "all" }
|
||||||
|
| { kind: "exact"; value: string }
|
||||||
|
| { kind: "regex"; value: RegExp };
|
||||||
|
|
||||||
|
function compilePattern(pattern: string): CompiledPattern {
|
||||||
|
const normalized = pattern.trim().toLowerCase();
|
||||||
|
if (!normalized) return { kind: "exact", value: "" };
|
||||||
|
if (normalized === "*") return { kind: "all" };
|
||||||
|
if (!normalized.includes("*")) return { kind: "exact", value: normalized };
|
||||||
|
const escaped = normalized.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
return {
|
||||||
|
kind: "regex",
|
||||||
|
value: new RegExp(`^${escaped.replaceAll("\\*", ".*")}$`),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function compilePatterns(patterns?: string[]): CompiledPattern[] {
|
||||||
|
if (!Array.isArray(patterns)) return [];
|
||||||
|
return expandToolGroups(patterns)
|
||||||
|
.map(compilePattern)
|
||||||
|
.filter((pattern) => pattern.kind !== "exact" || pattern.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesAny(name: string, patterns: CompiledPattern[]): boolean {
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
if (pattern.kind === "all") return true;
|
||||||
|
if (pattern.kind === "exact" && name === pattern.value) return true;
|
||||||
|
if (pattern.kind === "regex" && pattern.value.test(name)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
export function isToolAllowed(policy: SandboxToolPolicy, name: string) {
|
export function isToolAllowed(policy: SandboxToolPolicy, name: string) {
|
||||||
const deny = new Set(expandToolGroups(policy.deny));
|
const normalized = name.trim().toLowerCase();
|
||||||
if (deny.has(name.toLowerCase())) return false;
|
const deny = compilePatterns(policy.deny);
|
||||||
const allow = expandToolGroups(policy.allow);
|
if (matchesAny(normalized, deny)) return false;
|
||||||
|
const allow = compilePatterns(policy.allow);
|
||||||
if (allow.length === 0) return true;
|
if (allow.length === 0) return true;
|
||||||
return allow.includes(name.toLowerCase());
|
return matchesAny(normalized, allow);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveSandboxToolPolicyForAgent(
|
export function resolveSandboxToolPolicyForAgent(
|
||||||
|
|||||||
Reference in New Issue
Block a user