fix(shell): avoid self-kill from pgrep selectors (#6544)

* fix(shell): avoid self-kill from pgrep selectors

Fixes #6246

* fix(shell): handle pgrep review cases
This commit is contained in:
易良 2026-07-09 07:25:52 +08:00 committed by GitHub
parent 0a54652e07
commit 10fa9effbb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 201 additions and 5 deletions

View file

@ -52,6 +52,7 @@ IMPORTANT: This tool is for terminal operations like git, npm, docker, etc. DO N
- Any command expected to run indefinitely until manually stopped
- Command is executed as a subprocess that leads its own process group. Command process group can be terminated as \`kill -- -PGID\` or signaled as \`kill -s SIGNAL -- -PGID\`.
- To stop a background command started by this tool, use \`task_stop\` when a task id is available. Do not use broad process-name kills such as \`kill $(pgrep node)\`, \`pkill node\`, or \`killall node\`; use a specific PID or process group id where supported.
- Use foreground execution (is_background: false) for:
- One-time commands: \`ls\`, \`cat\`, \`grep\`
- Build commands: \`npm run build\`, \`make\`
@ -106,6 +107,7 @@ IMPORTANT: This tool is for terminal operations like git, npm, docker, etc. DO N
- Web servers: \`python -m http.server\`, \`php -S localhost:8000\`
- Any command expected to run indefinitely until manually stopped
- To stop a background command started by this tool, use \`task_stop\` when a task id is available. Do not use broad process-name kills such as \`kill $(pgrep node)\`, \`pkill node\`, or \`killall node\`; use a specific PID or process group id where supported.
- Use foreground execution (is_background: false) for:
- One-time commands: \`ls\`, \`cat\`, \`grep\`
- Build commands: \`npm run build\`, \`make\`

View file

@ -4740,6 +4740,8 @@ function getShellToolDescription(): string {
const processGroupNote = isWindows
? ''
: '\n - Command is executed as a subprocess that leads its own process group. Command process group can be terminated as `kill -- -PGID` or signaled as `kill -s SIGNAL -- -PGID`.';
const processStopNote =
'\n - To stop a background command started by this tool, use `task_stop` when a task id is available. Do not use broad process-name kills such as `kill $(pgrep node)`, `pkill node`, or `killall node`; use a specific PID or process group id where supported.';
return `Executes a given shell command (as \`${executionWrapper}\`) in a subprocess with optional timeout, ensuring proper handling and security measures.
@ -4775,7 +4777,7 @@ ${getShellCommandSequencingGuidance(shellConfiguration)}
- Database servers: \`mongod\`, \`mysql\`, \`redis-server\`
- Web servers: \`python -m http.server\`, \`php -S localhost:8000\`
- Any command expected to run indefinitely until manually stopped
${processGroupNote}
${processGroupNote}${processStopNote}
- Use foreground execution (is_background: false) for:
- One-time commands: \`ls\`, \`cat\`, \`grep\`
- Build commands: \`npm run build\`, \`make\`

View file

@ -606,6 +606,18 @@ describe('detectSelfKillCommand', () => {
expect(detectSelfKillCommand('command -p killall node')).toBe(true);
});
it('detects kill commands using pgrep selectors for qwen-code hosts', () => {
expect(detectSelfKillCommand('kill -9 $(pgrep node)')).toBe(true);
expect(detectSelfKillCommand('kill $(pgrep -f node)')).toBe(true);
expect(detectSelfKillCommand('kill -9 $(pgrep node | head -1)')).toBe(true);
expect(detectSelfKillCommand('kill -9 `pgrep node | head -1`')).toBe(true);
expect(detectSelfKillCommand('pgrep node | xargs kill')).toBe(true);
expect(detectSelfKillCommand('pgrep node | xargs sudo kill')).toBe(true);
expect(detectSelfKillCommand('pgrep node | xargs -I {} kill -9 {}')).toBe(
true,
);
});
it('detects taskkill inline and dash-prefixed image options', () => {
expect(detectSelfKillCommand('taskkill /IM:node.exe /F')).toBe(true);
expect(
@ -648,6 +660,10 @@ describe('detectSelfKillCommand', () => {
expect(detectSelfKillCommand('pkill -f vite')).toBe(false);
expect(detectSelfKillCommand('pkill -f "node server.js"')).toBe(false);
expect(detectSelfKillCommand('pkill -9f "node server.js"')).toBe(false);
expect(detectSelfKillCommand('kill -9 $(pgrep vite)')).toBe(false);
expect(detectSelfKillCommand('kill -9 $(pgrep -f "node server.js")')).toBe(
false,
);
expect(detectSelfKillCommand('pkill -F qwen-code.pid vite')).toBe(false);
expect(detectSelfKillCommand('taskkill /IM notepad.exe')).toBe(false);
});

View file

@ -208,6 +208,8 @@ export function splitCommands(command: string): string[] {
let currentCommand = '';
let inSingleQuotes = false;
let inDoubleQuotes = false;
let inBackticks = false;
let substitutionDepth = 0;
let i = 0;
const previousNonWhitespaceChar = (index: number): string | undefined => {
@ -235,13 +237,42 @@ export function splitCommands(command: string): string[] {
continue;
}
if (char === "'" && !inDoubleQuotes) {
if (!inSingleQuotes && char === '`') {
inBackticks = !inBackticks;
} else if (
!inSingleQuotes &&
!inBackticks &&
char === '$' &&
nextChar === '('
) {
substitutionDepth++;
currentCommand += '$(';
i += 2;
continue;
} else if (!inBackticks && substitutionDepth > 0 && char === ')') {
substitutionDepth--;
} else if (
!inBackticks &&
substitutionDepth === 0 &&
char === "'" &&
!inDoubleQuotes
) {
inSingleQuotes = !inSingleQuotes;
} else if (char === '"' && !inSingleQuotes) {
} else if (
!inBackticks &&
substitutionDepth === 0 &&
char === '"' &&
!inSingleQuotes
) {
inDoubleQuotes = !inDoubleQuotes;
}
if (!inSingleQuotes && !inDoubleQuotes) {
if (
!inSingleQuotes &&
!inDoubleQuotes &&
!inBackticks &&
substitutionDepth === 0
) {
if (
(char === '&' && nextChar === '&') ||
(char === '|' && (nextChar === '|' || nextChar === '&'))
@ -460,6 +491,28 @@ const PKILL_OPTIONS_WITH_VALUES = new Set([
'--euid',
]);
const XARGS_OPTIONS_WITH_VALUES = new Set([
'-I',
'-n',
'-P',
'-s',
'-E',
'-d',
'-L',
'-l',
'-a',
'-J',
'-R',
'--replace',
'--max-args',
'--max-procs',
'--max-chars',
'--eof',
'--delimiter',
'--max-lines',
'--arg-file',
]);
export const SHELL_SELF_KILL_REJECTION =
'Blocked: this command may terminate the running qwen-code process because it targets all node/qwen-code processes. Use task_stop for managed background shells, or kill a specific PID instead.';
@ -736,13 +789,126 @@ function pkillTargetsSelf(tokens: string[]): boolean {
return args.some((arg) => matchesSelfProcessPattern(arg));
}
function pgrepTargetsSelf(tokens: string[]): boolean {
return pkillTargetsSelf(['pkill', ...tokens.slice(1)]);
}
function pgrepSubstitutionArgs(segment: string): string[] {
let quote: '"' | "'" | '' = '';
let escaped = false;
const args: string[] = [];
for (let index = 0; index < segment.length; index++) {
const char = segment[index]!;
if (quote === "'") {
if (char === "'") quote = '';
continue;
}
if (escaped) {
escaped = false;
continue;
}
if (char === '\\') {
escaped = true;
continue;
}
if (char === '"') {
quote = quote === '"' ? '' : '"';
continue;
}
if (char === "'" && quote === '') {
quote = "'";
continue;
}
if (char === '`') {
let end = index + 1;
let innerEscaped = false;
for (; end < segment.length; end++) {
const innerChar = segment[end]!;
if (innerEscaped) {
innerEscaped = false;
continue;
}
if (innerChar === '\\') {
innerEscaped = true;
continue;
}
if (innerChar === '`') break;
}
if (end >= segment.length) {
break;
}
const inner = segment.slice(index + 1, end).trim();
const match = inner.match(/^pgrep\b(.*)$/i);
if (match) {
args.push(match[1] ?? '');
}
index = end;
continue;
}
if (char !== '$' || segment[index + 1] !== '(') {
continue;
}
const end = segment.indexOf(')', index + 2);
if (end === -1) {
break;
}
const inner = segment.slice(index + 2, end).trim();
const match = inner.match(/^pgrep\b(.*)$/i);
if (match) {
args.push(match[1] ?? '');
}
index = end;
}
return args;
}
function killCommandTargetsSelf(segment: string): boolean {
return pgrepSubstitutionArgs(segment).some((args) => {
const parsed = parseShellSegment(`pgrep ${args}`);
return parsed !== null && pgrepTargetsSelf(parsed);
});
}
function xargsInvokesKill(segment: string | undefined): boolean {
if (!segment) {
return false;
}
const parsed = parseShellSegment(segment);
if (!parsed) {
return false;
}
const tokens = unwrapExecutionPrefixes(parsed);
if (normalizeExecutableName(tokens[0] ?? '') !== 'xargs') {
return false;
}
const commandTokens = unwrapExecutionPrefixes(
commandArguments(tokens, XARGS_OPTIONS_WITH_VALUES),
);
const command = commandTokens.find((token) => !isOptionToken(token));
return normalizeExecutableName(command ?? '') === 'kill';
}
export function detectSelfKillCommand(command: string): boolean {
if (!/kill/i.test(command)) {
return false;
}
const segments = getCommandSegments(command);
for (const segment of segments) {
for (let index = 0; index < segments.length; index++) {
const segment = segments[index]!;
const parsed = parseShellSegment(segment);
if (!parsed) {
continue;
@ -763,6 +929,16 @@ export function detectSelfKillCommand(command: string): boolean {
if (root === 'pkill' && pkillTargetsSelf(tokens)) {
return true;
}
if (root === 'kill' && killCommandTargetsSelf(segment)) {
return true;
}
if (root === 'pgrep' && pgrepTargetsSelf(tokens)) {
for (let j = index + 1; j < segments.length; j++) {
if (xargsInvokesKill(segments[j])) {
return true;
}
}
}
}
return false;