mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-24 18:52:29 -07:00
fix(policy): support regex anchors in commandRegex
Modified buildArgsPatterns to correctly transform ^ and $ anchors into JSON-aware patterns, allowing precise matching of command values. Closes #19688
This commit is contained in:
@@ -1482,6 +1482,58 @@ describe('PolicyEngine', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('commandRegex anchors', () => {
|
||||
it('should ALLOW tmux command with $ anchor', async () => {
|
||||
const patterns = buildArgsPatterns(
|
||||
undefined,
|
||||
undefined,
|
||||
'tmux send-keys -t [a-z0-9:]+ (C-c|Up|Enter|Up Enter)$',
|
||||
);
|
||||
const regex = new RegExp(patterns[0]!);
|
||||
|
||||
engine.addRule({
|
||||
toolName: 'run_shell_command',
|
||||
decision: PolicyDecision.ALLOW,
|
||||
priority: 100,
|
||||
argsPattern: regex,
|
||||
});
|
||||
|
||||
const toolCall = {
|
||||
name: 'run_shell_command',
|
||||
args: {
|
||||
command: 'tmux send-keys -t superpowers:6 C-c',
|
||||
},
|
||||
};
|
||||
|
||||
const result = await engine.check(toolCall, undefined);
|
||||
|
||||
expect(result.decision).toBe(PolicyDecision.ALLOW);
|
||||
});
|
||||
|
||||
it('should ALLOW git status with ^ anchor', async () => {
|
||||
const patterns = buildArgsPatterns(undefined, undefined, '^git status');
|
||||
const regex = new RegExp(patterns[0]!);
|
||||
|
||||
engine.addRule({
|
||||
toolName: 'run_shell_command',
|
||||
decision: PolicyDecision.ALLOW,
|
||||
priority: 100,
|
||||
argsPattern: regex,
|
||||
});
|
||||
|
||||
const toolCall = {
|
||||
name: 'run_shell_command',
|
||||
args: {
|
||||
command: 'git status',
|
||||
},
|
||||
};
|
||||
|
||||
const result = await engine.check(toolCall, undefined);
|
||||
|
||||
expect(result.decision).toBe(PolicyDecision.ALLOW);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Plan Mode vs Subagent Priority (Regression)', () => {
|
||||
it('should DENY subagents in Plan Mode despite dynamic allow rules', async () => {
|
||||
// Plan Mode Deny (1.06) > Subagent Allow (1.05)
|
||||
|
||||
@@ -111,7 +111,7 @@ priority = 100
|
||||
expect(result.errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should NOT match if ^ is used in commandRegex because it matches against full JSON', async () => {
|
||||
it('should match if ^ is used in commandRegex (TDD: currently fails)', async () => {
|
||||
const result = await runLoadPoliciesFromToml(`
|
||||
[[rule]]
|
||||
toolName = "run_shell_command"
|
||||
@@ -121,11 +121,10 @@ priority = 100
|
||||
`);
|
||||
|
||||
expect(result.rules).toHaveLength(1);
|
||||
// The generated pattern is "command":"^git status
|
||||
// This will NOT match '{"command":"git status"}' because of the '{"' at the start.
|
||||
// After the fix, this should match correctly
|
||||
expect(
|
||||
result.rules[0].argsPattern?.test('{"command":"git status"}'),
|
||||
).toBe(false);
|
||||
).toBe(true);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
|
||||
@@ -125,7 +125,6 @@ describe('policy/utils', () => {
|
||||
const validJsonArgs = '{"command":"echo \\"foo\\""}';
|
||||
expect(regex.test(validJsonArgs)).toBe(true);
|
||||
});
|
||||
|
||||
it('should NOT match prefixes followed by raw backslashes (security check)', () => {
|
||||
// Testing that we blocked the hole: "echo\foo"
|
||||
const prefix = 'echo ';
|
||||
@@ -144,5 +143,55 @@ describe('policy/utils', () => {
|
||||
const gitAttack = '{"command":"git\\\\status"}';
|
||||
expect(gitRegex.test(gitAttack)).toBe(false);
|
||||
});
|
||||
|
||||
describe('commandRegex anchors', () => {
|
||||
it('should transform ^ anchor correctly (TDD: currently failing to match)', () => {
|
||||
const patterns = buildArgsPatterns(undefined, undefined, '^git status');
|
||||
const regex = new RegExp(patterns[0]!);
|
||||
// JSON stringified command: {"command":"git status"}
|
||||
const json = '{"command":"git status"}';
|
||||
// This CURRENTLY fails because the pattern is "command":"^git status
|
||||
expect(regex.test(json)).toBe(true);
|
||||
});
|
||||
|
||||
it('should transform $ anchor correctly (TDD: currently failing to match)', () => {
|
||||
const patterns = buildArgsPatterns(
|
||||
undefined,
|
||||
undefined,
|
||||
'tmux send-keys -t [a-z0-9:]+ (C-c|Up|Enter|Up Enter)$',
|
||||
);
|
||||
const regex = new RegExp(patterns[0]!);
|
||||
const json = '{"command":"tmux send-keys -t superpowers:6 C-c"}';
|
||||
// This CURRENTLY fails because $ matches end of JSON string, not end of command value
|
||||
expect(regex.test(json)).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle $ anchor when other fields follow (TDD: currently failing)', () => {
|
||||
const patterns = buildArgsPatterns(undefined, undefined, 'git status$');
|
||||
const regex = new RegExp(patterns[0]!);
|
||||
const json = '{"command":"git status","dir_path":"/tmp"}';
|
||||
expect(regex.test(json)).toBe(true);
|
||||
});
|
||||
|
||||
it('should NOT match if $ anchor is used and more text follows in command', () => {
|
||||
const patterns = buildArgsPatterns(undefined, undefined, 'git status$');
|
||||
const regex = new RegExp(patterns[0]!);
|
||||
const json = '{"command":"git status --porcelain"}';
|
||||
expect(regex.test(json)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle escaped anchors as literals', () => {
|
||||
const patterns = buildArgsPatterns(
|
||||
undefined,
|
||||
undefined,
|
||||
'git status\\$',
|
||||
);
|
||||
const regex = new RegExp(patterns[0]!);
|
||||
// Literal $ in command: git status$
|
||||
// JSON: {"command":"git status$"}
|
||||
const json = '{"command":"git status$"}';
|
||||
expect(regex.test(json)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -77,7 +77,37 @@ export function buildArgsPatterns(
|
||||
}
|
||||
|
||||
if (commandRegex) {
|
||||
return [`"command":"${commandRegex}`];
|
||||
let pattern = commandRegex;
|
||||
|
||||
// 1. Handle ^ (Start Anchor)
|
||||
// If the regex starts with ^, we remove it because the pattern is already
|
||||
// implicitly anchored to the start of the command value by the prepended
|
||||
// "command":" prefix. We only do this if it's not escaped.
|
||||
if (pattern.startsWith('^')) {
|
||||
pattern = pattern.slice(1);
|
||||
}
|
||||
|
||||
// 2. Handle $ (End Anchor)
|
||||
// If the regex ends with $, we replace it with "(?:,|\\}) to match the
|
||||
// closing quote of the command value in the JSON-stringified arguments.
|
||||
// We only do this if the $ is not escaped by an odd number of backslashes.
|
||||
if (pattern.endsWith('$')) {
|
||||
let backslashCount = 0;
|
||||
for (let i = pattern.length - 2; i >= 0; i--) {
|
||||
if (pattern[i] === '\\') {
|
||||
backslashCount++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (backslashCount % 2 === 0) {
|
||||
// Anchor to the end of the JSON string value: " followed by , or }
|
||||
pattern = pattern.slice(0, -1) + '"(?:,|\\})';
|
||||
}
|
||||
}
|
||||
|
||||
return [`"command":"${pattern}`];
|
||||
}
|
||||
|
||||
return [argsPattern];
|
||||
|
||||
Reference in New Issue
Block a user