chore: remove reverse engineering of arg pattern

This commit is contained in:
Jack Wotherspoon
2026-03-14 20:44:42 +01:00
parent 3de096b444
commit 4204813895
10 changed files with 235 additions and 313 deletions
@@ -20,6 +20,7 @@ function makeRule(
priority: 0,
source: undefined,
argsPattern: undefined,
constraintDisplay: undefined,
...overrides,
} as PolicyRule;
}
@@ -32,21 +33,21 @@ const ALLOW_RULES: PolicyRule[] = [
toolName: 'run_shell_command',
priority: 4.1,
source: 'User: allowed-tools.toml',
argsPattern: new RegExp('"command":"git\\ show(?:[\\s"]|\\\\")'),
constraintDisplay: 'git show*',
}),
makeRule({
decision: PolicyDecision.ALLOW,
toolName: 'run_shell_command',
priority: 4.1,
source: 'User: allowed-tools.toml',
argsPattern: new RegExp('"command":"git\\ diff(?:[\\s"]|\\\\")'),
constraintDisplay: 'git diff*',
}),
makeRule({
decision: PolicyDecision.ALLOW,
toolName: 'run_shell_command',
priority: 4.0,
source: 'Workspace: .gemini/settings.json',
argsPattern: new RegExp('"command":"npm\\ test(?:[\\s"]|\\\\")'),
constraintDisplay: 'npm test*',
}),
];
@@ -71,7 +72,7 @@ const DENY_RULES: PolicyRule[] = [
toolName: 'run_shell_command',
priority: 10,
source: 'Admin: admin-policies.toml',
argsPattern: new RegExp('"command":"rm\\ -rf(?:[\\s"]|\\\\")'),
constraintDisplay: 'rm -rf*',
}),
];
+5 -67
View File
@@ -5,71 +5,9 @@
*/
import { describe, it, expect } from 'vitest';
import { formatArgsPattern, buildPolicyListItems } from './policyUtils.js';
import { buildPolicyListItems } from './policyUtils.js';
import { PolicyDecision } from '@google/gemini-cli-core';
describe('formatArgsPattern', () => {
it('should return undefined for undefined argsPattern', () => {
expect(formatArgsPattern(undefined)).toBeUndefined();
});
it('should parse commandPrefix pattern (git diff)', () => {
// buildArgsPatterns produces this for commandPrefix: "git diff"
const pattern = new RegExp('"command":"git\\ diff(?:[\\s"]|\\\\")');
expect(formatArgsPattern(pattern)).toBe('git diff*');
});
it('should parse commandPrefix with escaped quotes (git show)', () => {
// buildArgsPatterns uses escapeRegex() which escapes quotes to \"
const pattern = new RegExp('\\"command\\":\\"git\\ show(?:[\\s"]|\\\\")');
expect(formatArgsPattern(pattern)).toBe('git show*');
});
it('should parse commandPrefix with special chars (npm run test:ci)', () => {
// escapeRegex escapes the colon
const pattern = new RegExp(
'"command":"npm\\ run\\ test\\:ci(?:[\\s"]|\\\\")',
);
expect(formatArgsPattern(pattern)).toBe('npm run test:ci*');
});
it('should parse commandPrefix with single word (ls)', () => {
const pattern = new RegExp('"command":"ls(?:[\\s"]|\\\\")');
expect(formatArgsPattern(pattern)).toBe('ls*');
});
it('should parse commandRegex pattern', () => {
const pattern = new RegExp('"command":"npm run .*');
expect(formatArgsPattern(pattern)).toBe('npm run .*');
});
it('should parse commandRegex with escaped quotes', () => {
const pattern = new RegExp('\\"command\\":\\"npm run .*');
expect(formatArgsPattern(pattern)).toBe('npm run .*');
});
it('should parse file_path pattern', () => {
const pattern = new RegExp('"file_path":".*\\/plans\\/.*"');
expect(formatArgsPattern(pattern)).toBe('path: .*\\/plans\\/.*');
});
it('should return file_path fallback for unrecognized file_path shape', () => {
const pattern = new RegExp('"file_path"');
expect(formatArgsPattern(pattern)).toBe('path: ...');
});
it('should return raw source for unknown short regex', () => {
const pattern = new RegExp('something_unknown');
expect(formatArgsPattern(pattern)).toBe('something_unknown');
});
it('should truncate long unknown patterns', () => {
const longPattern = new RegExp('a'.repeat(50));
const result = formatArgsPattern(longPattern);
expect(result).toBe('a'.repeat(40) + '...');
});
});
describe('buildPolicyListItems', () => {
const toolDisplayNames = new Map([
['run_shell_command', 'Shell'],
@@ -145,7 +83,7 @@ describe('buildPolicyListItems', () => {
{
decision: PolicyDecision.ALLOW,
toolName: 'run_shell_command',
argsPattern: new RegExp('"command":"git\\ diff(?:[\\s"]|\\\\")'),
constraintDisplay: 'git diff*',
},
];
@@ -175,12 +113,12 @@ describe('buildPolicyListItems', () => {
expect(items[0].searchText).toContain('run_shell_command');
});
it('should format constraint from argsPattern', () => {
it('should take constraint from constraintDisplay', () => {
const rules = [
{
decision: PolicyDecision.ALLOW,
toolName: 'run_shell_command',
argsPattern: new RegExp('"command":"git\\ show(?:[\\s"]|\\\\")'),
constraintDisplay: 'git show*',
},
];
@@ -192,7 +130,7 @@ describe('buildPolicyListItems', () => {
expect(items[0].constraint).toBe('git show*');
});
it('should have undefined constraint when no argsPattern', () => {
it('should have undefined constraint when no constraintDisplay', () => {
const rules = [
{ decision: PolicyDecision.ALLOW, toolName: 'glob', priority: 5 },
];
+1 -75
View File
@@ -28,80 +28,6 @@ export interface PolicyListItem {
searchText: string;
}
/**
* Un-escapes a regex string that was escaped by `escapeRegex()` from
* packages/core/src/policy/utils.ts.
*
* escapeRegex escapes: [-[\]{}()*+?.,\\^$|#\s"]
* by prefixing each with a backslash.
*/
function unescapeRegex(escaped: string): string {
return escaped.replace(/\\(.)/g, '$1');
}
/**
* Formats an argsPattern regex into a human-readable constraint string.
*
* The policy engine compiles TOML rule fields (commandPrefix, commandRegex,
* argsPattern) into RegExp objects via buildArgsPatterns() in
* packages/core/src/policy/utils.ts. This function reverses that compilation
* back into readable text for display.
*
* Pattern formats (checked in order):
*
* 1. commandPrefix `"command":"<escaped>(?:[\s"]|\\")` `<prefix>*`
* 2. commandRegex starts with `"command":"` raw regex after the prefix
* 3. file_path contains `"file_path":"<path>"` `path: <path>`
* 4. Fallback truncated raw regex source
*
* Returns undefined if there is no constraint (tool-only or wildcard rules).
*/
export function formatArgsPattern(
argsPattern: RegExp | undefined,
): string | undefined {
if (!argsPattern) {
return undefined;
}
const source = argsPattern.source;
// buildArgsPatterns uses escapeRegex() which escapes quotes to \", so the
// regex source contains escaped quotes. We support both escaped (\") and
// unescaped (") formats for robustness.
const cmdPrefix = /^\\?"command\\?":\\?"(.+?)\(\?:\[\\s"]\|\\\\"?\)$/;
// 1. commandPrefix: \"command\":\"<escaped-prefix>(?:[\s"]|\\")"
// The lookahead ensures the prefix is word-bounded in JSON.
const prefixMatch = source.match(cmdPrefix);
if (prefixMatch) {
return unescapeRegex(prefixMatch[1]) + '*';
}
// 2. commandRegex: starts with "command":" or \"command\":\"
const cmdRegexPrefix = /^\\?"command\\?":\\?"/;
const cmdRegexMatch = source.match(cmdRegexPrefix);
if (cmdRegexMatch) {
const regex = source.slice(cmdRegexMatch[0].length);
return regex;
}
// 3. file_path pattern
if (source.includes('"file_path"')) {
const pathMatch = source.match(/"file_path":"(.+?)"/);
if (pathMatch) {
return `path: ${pathMatch[1]}`;
}
return 'path: ...';
}
// 4. Fallback: truncated raw regex
const maxLen = 40;
if (source.length > maxLen) {
return source.substring(0, maxLen) + '...';
}
return source;
}
/**
* Builds a list of PolicyListItems from rules, filtered by decision and sorted
* by priority descending.
@@ -119,7 +45,7 @@ export function buildPolicyListItems(
const toolDisplayName = rule.toolName
? (toolDisplayNames.get(rule.toolName) ?? rule.toolName)
: 'all tools';
const constraint = formatArgsPattern(rule.argsPattern);
const constraint = rule.constraintDisplay;
const priority = String(rule.priority ?? 0);
const source = rule.source ?? '';