feat(cli): deprecate --allowed-tools and excludeTools in favor of policy engine (#18508)

This commit is contained in:
Abhijit Balaji
2026-02-11 16:49:48 -08:00
committed by GitHub
parent c370d2397b
commit 0e85e021dc
9 changed files with 327 additions and 39 deletions
+103 -3
View File
@@ -26,6 +26,22 @@ import {
} from '../utils/shell-utils.js';
import { getToolAliases } from '../tools/tool-names.js';
function isWildcardPattern(name: string): boolean {
return name.endsWith('__*');
}
function getWildcardPrefix(pattern: string): string {
return pattern.slice(0, -3);
}
function matchesWildcard(pattern: string, toolName: string): boolean {
if (!isWildcardPattern(pattern)) {
return false;
}
const prefix = getWildcardPrefix(pattern);
return toolName.startsWith(prefix + '__');
}
function ruleMatches(
rule: PolicyRule | SafetyCheckerRule,
toolCall: FunctionCall,
@@ -43,8 +59,8 @@ function ruleMatches(
// Check tool name if specified
if (rule.toolName) {
// Support wildcard patterns: "serverName__*" matches "serverName__anyTool"
if (rule.toolName.endsWith('__*')) {
const prefix = rule.toolName.slice(0, -3); // Remove "__*"
if (isWildcardPattern(rule.toolName)) {
const prefix = getWildcardPrefix(rule.toolName);
if (serverName !== undefined) {
// Robust check: if serverName is provided, it MUST match the prefix exactly.
// This prevents "malicious-server" from spoofing "trusted-server" by naming itself "trusted-server__malicious".
@@ -53,7 +69,7 @@ function ruleMatches(
}
}
// Always verify the prefix, even if serverName matched
if (!toolCall.name || !toolCall.name.startsWith(prefix + '__')) {
if (!toolCall.name || !matchesWildcard(rule.toolName, toolCall.name)) {
return false;
}
} else if (toolCall.name !== rule.toolName) {
@@ -509,6 +525,90 @@ export class PolicyEngine {
return this.hookCheckers;
}
/**
* Get tools that are effectively denied by the current rules.
* This takes into account:
* 1. Global rules (no argsPattern)
* 2. Priority order (higher priority wins)
* 3. Non-interactive mode (ASK_USER becomes DENY)
*/
getExcludedTools(): Set<string> {
const excludedTools = new Set<string>();
const processedTools = new Set<string>();
let globalVerdict: PolicyDecision | undefined;
for (const rule of this.rules) {
// We only care about rules without args pattern for exclusion from the model
if (rule.argsPattern) {
continue;
}
// Check if rule applies to current approval mode
if (rule.modes && rule.modes.length > 0) {
if (!rule.modes.includes(this.approvalMode)) {
continue;
}
}
// Handle Global Rules
if (!rule.toolName) {
if (globalVerdict === undefined) {
globalVerdict = rule.decision;
if (globalVerdict !== PolicyDecision.DENY) {
// Global ALLOW/ASK found.
// Since rules are sorted by priority, this overrides any lower-priority rules.
// We can stop processing because nothing else will be excluded.
break;
}
// If Global DENY, we continue to find specific tools to add to excluded set
}
continue;
}
const toolName = rule.toolName;
// Check if already processed (exact match)
if (processedTools.has(toolName)) {
continue;
}
// Check if covered by a processed wildcard
let coveredByWildcard = false;
for (const processed of processedTools) {
if (
isWildcardPattern(processed) &&
matchesWildcard(processed, toolName)
) {
// It's covered by a higher-priority wildcard rule.
// If that wildcard rule resulted in exclusion, this tool should also be excluded.
if (excludedTools.has(processed)) {
excludedTools.add(toolName);
}
coveredByWildcard = true;
break;
}
}
if (coveredByWildcard) {
continue;
}
processedTools.add(toolName);
// Determine decision
let decision: PolicyDecision;
if (globalVerdict !== undefined) {
decision = globalVerdict;
} else {
decision = rule.decision;
}
if (decision === PolicyDecision.DENY) {
excludedTools.add(toolName);
}
}
return excludedTools;
}
private applyNonInteractiveMode(decision: PolicyDecision): PolicyDecision {
// In non-interactive mode, ASK_USER becomes DENY
if (this.nonInteractive && decision === PolicyDecision.ASK_USER) {