Fix shell auto-approval parsing for chained commands (#11527)

This commit is contained in:
cornmander
2025-10-20 19:18:00 -04:00
committed by GitHub
parent 31f58a1f04
commit 70a99af1d3
4 changed files with 250 additions and 6 deletions
@@ -21,8 +21,10 @@ import {
isCommandAllowed,
initializeShellParsers,
stripShellWrapper,
isShellInvocationAllowlisted,
} from './shell-utils.js';
import type { Config } from '../config/config.js';
import type { AnyToolInvocation } from '../index.js';
const mockPlatform = vi.hoisted(() => vi.fn());
const mockHomedir = vi.hoisted(() => vi.fn());
@@ -420,6 +422,53 @@ describe('stripShellWrapper', () => {
});
});
describe('isShellInvocationAllowlisted', () => {
function createInvocation(command: string): AnyToolInvocation {
return { params: { command } } as unknown as AnyToolInvocation;
}
it('should return false when any chained command segment is not allowlisted', () => {
const invocation = createInvocation(
'git status && rm -rf /tmp/should-not-run',
);
expect(
isShellInvocationAllowlisted(invocation, ['run_shell_command(git)']),
).toBe(false);
});
it('should return true when every segment is explicitly allowlisted', () => {
const invocation = createInvocation(
'git status && rm -rf /tmp/should-run && git diff',
);
expect(
isShellInvocationAllowlisted(invocation, [
'run_shell_command(git)',
'run_shell_command(rm -rf)',
]),
).toBe(true);
});
it('should return true when the allowlist contains a wildcard shell entry', () => {
const invocation = createInvocation('git status && rm -rf /tmp/should-run');
expect(
isShellInvocationAllowlisted(invocation, ['run_shell_command']),
).toBe(true);
});
it('should treat piped commands as separate segments that must be allowlisted', () => {
const invocation = createInvocation('git status | tail -n 1');
expect(
isShellInvocationAllowlisted(invocation, ['run_shell_command(git)']),
).toBe(false);
expect(
isShellInvocationAllowlisted(invocation, [
'run_shell_command(git)',
'run_shell_command(tail)',
]),
).toBe(true);
});
});
describe('escapeShellArg', () => {
describe('POSIX (bash)', () => {
it('should use shell-quote for escaping', () => {
+72
View File
@@ -738,3 +738,75 @@ export function isCommandAllowed(
}
return { allowed: false, reason: blockReason };
}
/**
* Determines whether a shell invocation should be auto-approved based on an allowlist.
*
* This reuses the same parsing logic as command-permission enforcement so that
* chained commands must be individually covered by the allowlist.
*
* @param invocation The shell tool invocation being evaluated.
* @param allowedPatterns The configured allowlist patterns (e.g. `run_shell_command(git)`).
* @returns True if every parsed command segment is allowed by the patterns; false otherwise.
*/
export function isShellInvocationAllowlisted(
invocation: AnyToolInvocation,
allowedPatterns: string[],
): boolean {
if (!allowedPatterns.length) {
return false;
}
const hasShellWildcard = allowedPatterns.some((pattern) =>
SHELL_TOOL_NAMES.includes(pattern),
);
const hasShellSpecificPattern = allowedPatterns.some((pattern) =>
SHELL_TOOL_NAMES.some((name) => pattern.startsWith(`${name}(`)),
);
if (!hasShellWildcard && !hasShellSpecificPattern) {
return false;
}
if (hasShellWildcard) {
return true;
}
if (
!('params' in invocation) ||
typeof invocation.params !== 'object' ||
invocation.params === null ||
!('command' in invocation.params)
) {
return false;
}
const commandValue = (invocation.params as { command?: unknown }).command;
if (typeof commandValue !== 'string' || !commandValue.trim()) {
return false;
}
const command = commandValue.trim();
const parseResult = parseCommandDetails(command);
if (!parseResult || parseResult.hasError) {
return false;
}
const normalize = (cmd: string): string => cmd.trim().replace(/\s+/g, ' ');
const commandsToValidate = parseResult.details
.map((detail) => normalize(detail.text))
.filter(Boolean);
if (commandsToValidate.length === 0) {
return false;
}
return commandsToValidate.every((commandSegment) =>
doesToolInvocationMatch(
SHELL_TOOL_NAMES[0],
{ params: { command: commandSegment } } as AnyToolInvocation,
allowedPatterns,
),
);
}