4
4
identify_files_added ,
5
5
identify_files_needed ,
6
6
} from "./utils/agent/apply-patch" ;
7
- import { loadConfig } from "./utils/config" ;
8
7
import * as path from "path" ;
9
8
import { parse } from "shell-quote" ;
10
9
@@ -72,13 +71,14 @@ export type ApprovalPolicy =
72
71
*/
73
72
export function canAutoApprove (
74
73
command : ReadonlyArray < string > ,
74
+ workdir : string | undefined ,
75
75
policy : ApprovalPolicy ,
76
76
writableRoots : ReadonlyArray < string > ,
77
77
env : NodeJS . ProcessEnv = process . env ,
78
78
) : SafetyAssessment {
79
79
if ( command [ 0 ] === "apply_patch" ) {
80
80
return command . length === 2 && typeof command [ 1 ] === "string"
81
- ? canAutoApproveApplyPatch ( command [ 1 ] , writableRoots , policy )
81
+ ? canAutoApproveApplyPatch ( command [ 1 ] , workdir , writableRoots , policy )
82
82
: {
83
83
type : "reject" ,
84
84
reason : "Invalid apply_patch command" ,
@@ -104,7 +104,12 @@ export function canAutoApprove(
104
104
) {
105
105
const applyPatchArg = tryParseApplyPatch ( command [ 2 ] ) ;
106
106
if ( applyPatchArg != null ) {
107
- return canAutoApproveApplyPatch ( applyPatchArg , writableRoots , policy ) ;
107
+ return canAutoApproveApplyPatch (
108
+ applyPatchArg ,
109
+ workdir ,
110
+ writableRoots ,
111
+ policy ,
112
+ ) ;
108
113
}
109
114
110
115
let bashCmd ;
@@ -136,8 +141,8 @@ export function canAutoApprove(
136
141
// bashCmd could be a mix of strings and operators, e.g.:
137
142
// "ls || (true && pwd)" => [ 'ls', { op: '||' }, '(', 'true', { op: '&&' }, 'pwd', ')' ]
138
143
// We try to ensure that *every* command segment is deemed safe and that
139
- // all operators belong to an allow‑ list. If so, the entire expression is
140
- // considered auto‑ approvable.
144
+ // all operators belong to an allow- list. If so, the entire expression is
145
+ // considered auto- approvable.
141
146
142
147
const shellSafe = isEntireShellExpressionSafe ( bashCmd ) ;
143
148
if ( shellSafe != null ) {
@@ -163,6 +168,7 @@ export function canAutoApprove(
163
168
164
169
function canAutoApproveApplyPatch (
165
170
applyPatchArg : string ,
171
+ workdir : string | undefined ,
166
172
writableRoots : ReadonlyArray < string > ,
167
173
policy : ApprovalPolicy ,
168
174
) : SafetyAssessment {
@@ -180,7 +186,13 @@ function canAutoApproveApplyPatch(
180
186
break ;
181
187
}
182
188
183
- if ( isWritePatchConstrainedToWritablePaths ( applyPatchArg , writableRoots ) ) {
189
+ if (
190
+ isWritePatchConstrainedToWritablePaths (
191
+ applyPatchArg ,
192
+ workdir ,
193
+ writableRoots ,
194
+ )
195
+ ) {
184
196
return {
185
197
type : "auto-approve" ,
186
198
reason : "apply_patch command is constrained to writable paths" ,
@@ -209,6 +221,7 @@ function canAutoApproveApplyPatch(
209
221
*/
210
222
function isWritePatchConstrainedToWritablePaths (
211
223
applyPatchArg : string ,
224
+ workdir : string | undefined ,
212
225
writableRoots : ReadonlyArray < string > ,
213
226
) : boolean {
214
227
// `identify_files_needed()` returns a list of files that will be modified or
@@ -223,35 +236,60 @@ function isWritePatchConstrainedToWritablePaths(
223
236
return (
224
237
allPathsConstrainedTowritablePaths (
225
238
identify_files_needed ( applyPatchArg ) ,
239
+ workdir ,
226
240
writableRoots ,
227
241
) &&
228
242
allPathsConstrainedTowritablePaths (
229
243
identify_files_added ( applyPatchArg ) ,
244
+ workdir ,
230
245
writableRoots ,
231
246
)
232
247
) ;
233
248
}
234
249
235
250
function allPathsConstrainedTowritablePaths (
236
251
candidatePaths : ReadonlyArray < string > ,
252
+ workdir : string | undefined ,
237
253
writableRoots : ReadonlyArray < string > ,
238
254
) : boolean {
239
255
return candidatePaths . every ( ( candidatePath ) =>
240
- isPathConstrainedTowritablePaths ( candidatePath , writableRoots ) ,
256
+ isPathConstrainedTowritablePaths ( candidatePath , workdir , writableRoots ) ,
241
257
) ;
242
258
}
243
259
244
260
/** If candidatePath is relative, it will be resolved against cwd. */
245
261
function isPathConstrainedTowritablePaths (
246
262
candidatePath : string ,
263
+ workdir : string | undefined ,
247
264
writableRoots : ReadonlyArray < string > ,
248
265
) : boolean {
249
- const candidateAbsolutePath = path . resolve ( candidatePath ) ;
266
+ const candidateAbsolutePath = resolvePathAgainstWorkdir (
267
+ candidatePath ,
268
+ workdir ,
269
+ ) ;
270
+
250
271
return writableRoots . some ( ( writablePath ) =>
251
272
pathContains ( writablePath , candidateAbsolutePath ) ,
252
273
) ;
253
274
}
254
275
276
+ /**
277
+ * If not already an absolute path, resolves `candidatePath` against `workdir`
278
+ * if specified; otherwise, against `process.cwd()`.
279
+ */
280
+ export function resolvePathAgainstWorkdir (
281
+ candidatePath : string ,
282
+ workdir : string | undefined ,
283
+ ) : string {
284
+ if ( path . isAbsolute ( candidatePath ) ) {
285
+ return candidatePath ;
286
+ } else if ( workdir != null ) {
287
+ return path . resolve ( workdir , candidatePath ) ;
288
+ } else {
289
+ return path . resolve ( candidatePath ) ;
290
+ }
291
+ }
292
+
255
293
/** Both `parent` and `child` must be absolute paths. */
256
294
function pathContains ( parent : string , child : string ) : boolean {
257
295
const relative = path . relative ( parent , child ) ;
@@ -297,24 +335,6 @@ export function isSafeCommand(
297
335
) : SafeCommandReason | null {
298
336
const [ cmd0 , cmd1 , cmd2 , cmd3 ] = command ;
299
337
300
- const config = loadConfig ( ) ;
301
- if ( config . safeCommands && Array . isArray ( config . safeCommands ) ) {
302
- for ( const safe of config . safeCommands ) {
303
- // safe: "npm test" → ["npm", "test"]
304
- const safeArr = typeof safe === "string" ? safe . trim ( ) . split ( / \s + / ) : [ ] ;
305
- if (
306
- safeArr . length > 0 &&
307
- safeArr . length <= command . length &&
308
- safeArr . every ( ( v , i ) => v === command [ i ] )
309
- ) {
310
- return {
311
- reason : "User-defined safe command" ,
312
- group : "User config" ,
313
- } ;
314
- }
315
- }
316
- }
317
-
318
338
switch ( cmd0 ) {
319
339
case "cd" :
320
340
return {
@@ -333,7 +353,7 @@ export function isSafeCommand(
333
353
} ;
334
354
case "true" :
335
355
return {
336
- reason : "No‑ op (true)" ,
356
+ reason : "No- op (true)" ,
337
357
group : "Utility" ,
338
358
} ;
339
359
case "echo" :
@@ -348,11 +368,20 @@ export function isSafeCommand(
348
368
reason : "Ripgrep search" ,
349
369
group : "Searching" ,
350
370
} ;
351
- case "find" :
352
- return {
353
- reason : "Find files or directories" ,
354
- group : "Searching" ,
355
- } ;
371
+ case "find" : {
372
+ // Certain options to `find` allow executing arbitrary processes, so we
373
+ // cannot auto-approve them.
374
+ if (
375
+ command . some ( ( arg : string ) => UNSAFE_OPTIONS_FOR_FIND_COMMAND . has ( arg ) )
376
+ ) {
377
+ break ;
378
+ } else {
379
+ return {
380
+ reason : "Find files or directories" ,
381
+ group : "Searching" ,
382
+ } ;
383
+ }
384
+ }
356
385
case "grep" :
357
386
return {
358
387
reason : "Text search (grep)" ,
@@ -440,12 +469,27 @@ function isValidSedNArg(arg: string | undefined): boolean {
440
469
return arg != null && / ^ ( \d + , ) ? \d + p $ / . test ( arg ) ;
441
470
}
442
471
472
+ const UNSAFE_OPTIONS_FOR_FIND_COMMAND : ReadonlySet < string > = new Set ( [
473
+ // Options that can execute arbitrary commands.
474
+ "-exec" ,
475
+ "-execdir" ,
476
+ "-ok" ,
477
+ "-okdir" ,
478
+ // Option that deletes matching files.
479
+ "-delete" ,
480
+ // Options that write pathnames to a file.
481
+ "-fls" ,
482
+ "-fprint" ,
483
+ "-fprint0" ,
484
+ "-fprintf" ,
485
+ ] ) ;
486
+
443
487
// ---------------- Helper utilities for complex shell expressions -----------------
444
488
445
- // A conservative allow‑ list of bash operators that do not, on their own, cause
489
+ // A conservative allow- list of bash operators that do not, on their own, cause
446
490
// side effects. Redirections (>, >>, <, etc.) and command substitution `$()`
447
491
// are intentionally excluded. Parentheses used for grouping are treated as
448
- // strings by `shell‑ quote`, so we do not add them here. Reference:
492
+ // strings by `shell- quote`, so we do not add them here. Reference:
449
493
// https://github.com/substack/node-shell-quote#parsecmd-opts
450
494
const SAFE_SHELL_OPERATORS : ReadonlySet < string > = new Set ( [
451
495
"&&" , // logical AND
@@ -471,7 +515,7 @@ function isEntireShellExpressionSafe(
471
515
}
472
516
473
517
try {
474
- // Collect command segments delimited by operators. `shell‑ quote` represents
518
+ // Collect command segments delimited by operators. `shell- quote` represents
475
519
// subshell grouping parentheses as literal strings "(" and ")"; treat them
476
520
// as unsafe to keep the logic simple (since subshells could introduce
477
521
// unexpected scope changes).
@@ -539,7 +583,7 @@ function isParseEntryWithOp(
539
583
return (
540
584
typeof entry === "object" &&
541
585
entry != null &&
542
- // Using the safe `in` operator keeps the check property‑ safe even when
586
+ // Using the safe `in` operator keeps the check property- safe even when
543
587
// `entry` is a `string`.
544
588
"op" in entry &&
545
589
typeof ( entry as { op ?: unknown } ) . op === "string"
0 commit comments