Skip to content

Commit 44ff040

Browse files
Merge pull request #1347 from ai-yang/audit/fix-bash-command-semantics
fix: 避免 && 短路时误用未执行命令的退出码语义
2 parents 3bb6b57 + 9be2d21 commit 44ff040

2 files changed

Lines changed: 43 additions & 1 deletion

File tree

packages/builtin-tools/src/tools/BashTool/__tests__/commandSemantics.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,34 @@ describe('interpretCommandResult', () => {
5959
expect(result.message).toBe('No matches found')
6060
})
6161

62+
test('does not use an unexecuted rg command for && short-circuit semantics', () => {
63+
const result = interpretCommandResult(
64+
'cd /definitely-missing && rg pattern .',
65+
1,
66+
'',
67+
'cd: /definitely-missing: No such file or directory',
68+
)
69+
expect(result.isError).toBe(true)
70+
expect(result.message).toBe('Command failed with exit code 1')
71+
})
72+
73+
test('does not use an unexecuted diff command for && short-circuit semantics', () => {
74+
const result = interpretCommandResult(
75+
"node -e 'process.exit(1)' && diff a.txt b.txt",
76+
1,
77+
'',
78+
'',
79+
)
80+
expect(result.isError).toBe(true)
81+
expect(result.message).toBe('Command failed with exit code 1')
82+
})
83+
84+
test('keeps fallback semantics for a command reached through ||', () => {
85+
const result = interpretCommandResult('false || rg pattern .', 1, '', '')
86+
expect(result.isError).toBe(false)
87+
expect(result.message).toBe('No matches found')
88+
})
89+
6290
// ─── rg (ripgrep) semantics ──────────────────────────────────────
6391
test('rg exit 1 means no matches (not error)', () => {
6492
const result = interpretCommandResult('rg pattern', 1, '', '')

packages/builtin-tools/src/tools/BashTool/commandSemantics.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
* For example, grep returns 1 when no matches are found, which is not an error condition.
66
*/
77

8-
import { splitCommand_DEPRECATED } from 'src/utils/bash/commands.js'
8+
import {
9+
splitCommand_DEPRECATED,
10+
splitCommandWithOperators,
11+
} from 'src/utils/bash/commands.js'
912

1013
export type CommandSemantic = (
1114
exitCode: number,
@@ -110,6 +113,17 @@ function extractBaseCommand(command: string): string {
110113
* May get it super wrong - don't depend on this for security
111114
*/
112115
function heuristicallyExtractBaseCommand(command: string): string {
116+
// With an && list, the syntactically last command may never have run. For
117+
// example, `cd /missing && rg needle .` exits 1 from `cd`, but applying rg's
118+
// "1 = no matches" semantics would turn that real failure into success.
119+
// The executor currently only exposes the aggregate exit code, so fall back
120+
// to the conservative default whenever an AND-list makes the executed
121+
// command ambiguous. Pipes remain safe: without pipefail their last command
122+
// determines the aggregate status and is always executed.
123+
if (splitCommandWithOperators(command).includes('&&')) {
124+
return ''
125+
}
126+
113127
const segments = splitCommand_DEPRECATED(command)
114128

115129
// Take the last command as that's what determines the exit code

0 commit comments

Comments
 (0)