diff --git a/src/agents/glob-pattern.ts b/src/agents/glob-pattern.ts new file mode 100644 index 00000000000..cfb9a5ce93f --- /dev/null +++ b/src/agents/glob-pattern.ts @@ -0,0 +1,56 @@ +export type CompiledGlobPattern = + | { kind: "all" } + | { kind: "exact"; value: string } + | { kind: "regex"; value: RegExp }; + +function escapeRegex(value: string) { + // Standard "escape string for regex literal" pattern. + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +export function compileGlobPattern(params: { + raw: string; + normalize: (value: string) => string; +}): CompiledGlobPattern { + const normalized = params.normalize(params.raw); + if (!normalized) { + return { kind: "exact", value: "" }; + } + if (normalized === "*") { + return { kind: "all" }; + } + if (!normalized.includes("*")) { + return { kind: "exact", value: normalized }; + } + return { + kind: "regex", + value: new RegExp(`^${escapeRegex(normalized).replaceAll("\\*", ".*")}$`), + }; +} + +export function compileGlobPatterns(params: { + raw?: string[] | undefined; + normalize: (value: string) => string; +}): CompiledGlobPattern[] { + if (!Array.isArray(params.raw)) { + return []; + } + return params.raw + .map((raw) => compileGlobPattern({ raw, normalize: params.normalize })) + .filter((pattern) => pattern.kind !== "exact" || pattern.value); +} + +export function matchesAnyGlobPattern(value: string, patterns: CompiledGlobPattern[]): boolean { + for (const pattern of patterns) { + if (pattern.kind === "all") { + return true; + } + if (pattern.kind === "exact" && value === pattern.value) { + return true; + } + if (pattern.kind === "regex" && pattern.value.test(value)) { + return true; + } + } + return false; +} diff --git a/src/agents/pi-extensions/context-pruning/tools.ts b/src/agents/pi-extensions/context-pruning/tools.ts index 1fbca70657c..b25b981cef5 100644 --- a/src/agents/pi-extensions/context-pruning/tools.ts +++ b/src/agents/pi-extensions/context-pruning/tools.ts @@ -1,69 +1,26 @@ import type { ContextPruningToolMatch } from "./settings.js"; +import { compileGlobPatterns, matchesAnyGlobPattern } from "../../glob-pattern.js"; -function normalizePatterns(patterns?: string[]): string[] { - if (!Array.isArray(patterns)) { - return []; - } - return patterns - .map((p) => - String(p ?? "") - .trim() - .toLowerCase(), - ) - .filter(Boolean); -} - -type CompiledPattern = - | { kind: "all" } - | { kind: "exact"; value: string } - | { kind: "regex"; value: RegExp }; - -function compilePattern(pattern: string): CompiledPattern { - if (pattern === "*") { - return { kind: "all" }; - } - if (!pattern.includes("*")) { - return { kind: "exact", value: pattern }; - } - - const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - const re = new RegExp(`^${escaped.replaceAll("\\*", ".*")}$`); - return { kind: "regex", value: re }; -} - -function compilePatterns(patterns?: string[]): CompiledPattern[] { - return normalizePatterns(patterns).map(compilePattern); -} - -function matchesAny(toolName: string, patterns: CompiledPattern[]): boolean { - for (const p of patterns) { - if (p.kind === "all") { - return true; - } - if (p.kind === "exact" && toolName === p.value) { - return true; - } - if (p.kind === "regex" && p.value.test(toolName)) { - return true; - } - } - return false; +function normalizeGlob(value: string) { + return String(value ?? "") + .trim() + .toLowerCase(); } export function makeToolPrunablePredicate( match: ContextPruningToolMatch, ): (toolName: string) => boolean { - const deny = compilePatterns(match.deny); - const allow = compilePatterns(match.allow); + const deny = compileGlobPatterns({ raw: match.deny, normalize: normalizeGlob }); + const allow = compileGlobPatterns({ raw: match.allow, normalize: normalizeGlob }); return (toolName: string) => { - const normalized = toolName.trim().toLowerCase(); - if (matchesAny(normalized, deny)) { + const normalized = normalizeGlob(toolName); + if (matchesAnyGlobPattern(normalized, deny)) { return false; } if (allow.length === 0) { return true; } - return matchesAny(normalized, allow); + return matchesAnyGlobPattern(normalized, allow); }; } diff --git a/src/agents/pi-tools.policy.ts b/src/agents/pi-tools.policy.ts index dffd98d4977..522a7b60b71 100644 --- a/src/agents/pi-tools.policy.ts +++ b/src/agents/pi-tools.policy.ts @@ -6,70 +6,30 @@ import { resolveChannelGroupToolsPolicy } from "../config/group-policy.js"; import { resolveThreadParentSessionKey } from "../sessions/session-key-utils.js"; import { normalizeMessageChannel } from "../utils/message-channel.js"; import { resolveAgentConfig, resolveAgentIdFromSessionKey } from "./agent-scope.js"; +import { compileGlobPatterns, matchesAnyGlobPattern } from "./glob-pattern.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); + const deny = compileGlobPatterns({ + raw: expandToolGroups(policy.deny ?? []), + normalize: normalizeToolName, + }); + const allow = compileGlobPatterns({ + raw: expandToolGroups(policy.allow ?? []), + normalize: normalizeToolName, + }); return (name: string) => { const normalized = normalizeToolName(name); - if (matchesAny(normalized, deny)) { + if (matchesAnyGlobPattern(normalized, deny)) { return false; } if (allow.length === 0) { return true; } - if (matchesAny(normalized, allow)) { + if (matchesAnyGlobPattern(normalized, allow)) { return true; } - if (normalized === "apply_patch" && matchesAny("exec", allow)) { + if (normalized === "apply_patch" && matchesAnyGlobPattern("exec", allow)) { return true; } return false; diff --git a/src/agents/sandbox/tool-policy.ts b/src/agents/sandbox/tool-policy.ts index ea632a39464..b50a363846b 100644 --- a/src/agents/sandbox/tool-policy.ts +++ b/src/agents/sandbox/tool-policy.ts @@ -5,67 +5,31 @@ import type { SandboxToolPolicySource, } from "./types.js"; import { resolveAgentConfig } from "../agent-scope.js"; +import { compileGlobPatterns, matchesAnyGlobPattern } from "../glob-pattern.js"; import { expandToolGroups } from "../tool-policy.js"; import { DEFAULT_TOOL_ALLOW, DEFAULT_TOOL_DENY } from "./constants.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; +function normalizeGlob(value: string) { + return value.trim().toLowerCase(); } export function isToolAllowed(policy: SandboxToolPolicy, name: string) { - const normalized = name.trim().toLowerCase(); - const deny = compilePatterns(policy.deny); - if (matchesAny(normalized, deny)) { + const normalized = normalizeGlob(name); + const deny = compileGlobPatterns({ + raw: expandToolGroups(policy.deny ?? []), + normalize: normalizeGlob, + }); + if (matchesAnyGlobPattern(normalized, deny)) { return false; } - const allow = compilePatterns(policy.allow); + const allow = compileGlobPatterns({ + raw: expandToolGroups(policy.allow ?? []), + normalize: normalizeGlob, + }); if (allow.length === 0) { return true; } - return matchesAny(normalized, allow); + return matchesAnyGlobPattern(normalized, allow); } export function resolveSandboxToolPolicyForAgent(