5
5
6
6
import { Writable } from 'stream'
7
7
import { getLogger } from '../../shared/logger/logger'
8
- import { fs } from '../../shared/fs/fs' // e.g. for getUserHomeDir()
8
+ import { fs } from '../../shared/fs/fs'
9
9
import { ChildProcess , ChildProcessOptions } from '../../shared/utilities/processUtils'
10
10
import { InvokeOutput , OutputKind , sanitizePath } from './toolShared'
11
+ import { split } from 'shlex'
11
12
12
- export const readOnlyCommands : string [ ] = [ 'ls' , 'cat' , 'echo' , 'pwd' , 'which' , 'head' , 'tail' ]
13
+ export enum CommandCategory {
14
+ ReadOnly ,
15
+ HighRisk ,
16
+ Destructive ,
17
+ }
18
+
19
+ export const dangerousPatterns = new Set ( [ '<(' , '$(' , '`' , '>' , '&&' , '||' ] )
20
+ export const commandCategories = new Map < string , CommandCategory > ( [
21
+ // ReadOnly commands
22
+ [ 'ls' , CommandCategory . ReadOnly ] ,
23
+ [ 'cat' , CommandCategory . ReadOnly ] ,
24
+ [ 'bat' , CommandCategory . ReadOnly ] ,
25
+ [ 'pwd' , CommandCategory . ReadOnly ] ,
26
+ [ 'echo' , CommandCategory . ReadOnly ] ,
27
+ [ 'file' , CommandCategory . ReadOnly ] ,
28
+ [ 'less' , CommandCategory . ReadOnly ] ,
29
+ [ 'more' , CommandCategory . ReadOnly ] ,
30
+ [ 'tree' , CommandCategory . ReadOnly ] ,
31
+ [ 'find' , CommandCategory . ReadOnly ] ,
32
+ [ 'top' , CommandCategory . ReadOnly ] ,
33
+ [ 'htop' , CommandCategory . ReadOnly ] ,
34
+ [ 'ps' , CommandCategory . ReadOnly ] ,
35
+ [ 'df' , CommandCategory . ReadOnly ] ,
36
+ [ 'du' , CommandCategory . ReadOnly ] ,
37
+ [ 'free' , CommandCategory . ReadOnly ] ,
38
+ [ 'uname' , CommandCategory . ReadOnly ] ,
39
+ [ 'date' , CommandCategory . ReadOnly ] ,
40
+ [ 'whoami' , CommandCategory . ReadOnly ] ,
41
+ [ 'which' , CommandCategory . ReadOnly ] ,
42
+ [ 'ping' , CommandCategory . ReadOnly ] ,
43
+ [ 'ifconfig' , CommandCategory . ReadOnly ] ,
44
+ [ 'ip' , CommandCategory . ReadOnly ] ,
45
+ [ 'netstat' , CommandCategory . ReadOnly ] ,
46
+ [ 'ss' , CommandCategory . ReadOnly ] ,
47
+ [ 'dig' , CommandCategory . ReadOnly ] ,
48
+ [ 'grep' , CommandCategory . ReadOnly ] ,
49
+ [ 'wc' , CommandCategory . ReadOnly ] ,
50
+ [ 'sort' , CommandCategory . ReadOnly ] ,
51
+ [ 'diff' , CommandCategory . ReadOnly ] ,
52
+ [ 'head' , CommandCategory . ReadOnly ] ,
53
+ [ 'tail' , CommandCategory . ReadOnly ] ,
54
+
55
+ // HighRisk commands
56
+ [ 'chmod' , CommandCategory . HighRisk ] ,
57
+ [ 'chown' , CommandCategory . HighRisk ] ,
58
+ [ 'mv' , CommandCategory . HighRisk ] ,
59
+ [ 'cp' , CommandCategory . HighRisk ] ,
60
+ [ 'ln' , CommandCategory . HighRisk ] ,
61
+ [ 'mount' , CommandCategory . HighRisk ] ,
62
+ [ 'umount' , CommandCategory . HighRisk ] ,
63
+ [ 'kill' , CommandCategory . HighRisk ] ,
64
+ [ 'killall' , CommandCategory . HighRisk ] ,
65
+ [ 'pkill' , CommandCategory . HighRisk ] ,
66
+ [ 'iptables' , CommandCategory . HighRisk ] ,
67
+ [ 'route' , CommandCategory . HighRisk ] ,
68
+ [ 'systemctl' , CommandCategory . HighRisk ] ,
69
+ [ 'service' , CommandCategory . HighRisk ] ,
70
+ [ 'crontab' , CommandCategory . HighRisk ] ,
71
+ [ 'at' , CommandCategory . HighRisk ] ,
72
+ [ 'tar' , CommandCategory . HighRisk ] ,
73
+ [ 'awk' , CommandCategory . HighRisk ] ,
74
+ [ 'sed' , CommandCategory . HighRisk ] ,
75
+ [ 'wget' , CommandCategory . HighRisk ] ,
76
+ [ 'curl' , CommandCategory . HighRisk ] ,
77
+ [ 'nc' , CommandCategory . HighRisk ] ,
78
+ [ 'ssh' , CommandCategory . HighRisk ] ,
79
+ [ 'scp' , CommandCategory . HighRisk ] ,
80
+ [ 'ftp' , CommandCategory . HighRisk ] ,
81
+ [ 'sftp' , CommandCategory . HighRisk ] ,
82
+ [ 'rsync' , CommandCategory . HighRisk ] ,
83
+ [ 'chroot' , CommandCategory . HighRisk ] ,
84
+ [ 'lsof' , CommandCategory . HighRisk ] ,
85
+ [ 'strace' , CommandCategory . HighRisk ] ,
86
+ [ 'gdb' , CommandCategory . HighRisk ] ,
87
+
88
+ // Destructive commands
89
+ [ 'rm' , CommandCategory . Destructive ] ,
90
+ [ 'dd' , CommandCategory . Destructive ] ,
91
+ [ 'mkfs' , CommandCategory . Destructive ] ,
92
+ [ 'fdisk' , CommandCategory . Destructive ] ,
93
+ [ 'shutdown' , CommandCategory . Destructive ] ,
94
+ [ 'reboot' , CommandCategory . Destructive ] ,
95
+ [ 'poweroff' , CommandCategory . Destructive ] ,
96
+ [ 'sudo' , CommandCategory . Destructive ] ,
97
+ [ 'su' , CommandCategory . Destructive ] ,
98
+ [ 'useradd' , CommandCategory . Destructive ] ,
99
+ [ 'userdel' , CommandCategory . Destructive ] ,
100
+ [ 'passwd' , CommandCategory . Destructive ] ,
101
+ [ 'visudo' , CommandCategory . Destructive ] ,
102
+ [ 'insmod' , CommandCategory . Destructive ] ,
103
+ [ 'rmmod' , CommandCategory . Destructive ] ,
104
+ [ 'modprobe' , CommandCategory . Destructive ] ,
105
+ [ 'apt' , CommandCategory . Destructive ] ,
106
+ [ 'yum' , CommandCategory . Destructive ] ,
107
+ [ 'dnf' , CommandCategory . Destructive ] ,
108
+ [ 'pacman' , CommandCategory . Destructive ] ,
109
+ [ 'perl' , CommandCategory . Destructive ] ,
110
+ [ 'python' , CommandCategory . Destructive ] ,
111
+ [ 'bash' , CommandCategory . Destructive ] ,
112
+ [ 'sh' , CommandCategory . Destructive ] ,
113
+ [ 'exec' , CommandCategory . Destructive ] ,
114
+ [ 'eval' , CommandCategory . Destructive ] ,
115
+ [ 'xargs' , CommandCategory . Destructive ] ,
116
+ ] )
13
117
export const maxBashToolResponseSize : number = 1024 * 1024 // 1MB
14
118
export const lineCount : number = 1024
15
- export const dangerousPatterns : string [ ] = [ '|' , '<(' , '$(' , '`' , '>' , '&&' , '||' ]
119
+ export const destructiveCommandWarningMessage = '⚠️ WARNING: Destructive command detected:\n\n'
120
+ export const highRiskCommandWarningMessage = '⚠️ WARNING: High risk command detected:\n\n'
16
121
17
122
export interface ExecuteBashParams {
18
123
command : string
19
124
cwd ?: string
20
125
}
21
126
127
+ export interface CommandValidation {
128
+ requiresAcceptance : boolean
129
+ warning ?: string
130
+ }
131
+
22
132
export class ExecuteBash {
23
133
private readonly command : string
24
134
private readonly workingDirectory ?: string
@@ -34,7 +144,7 @@ export class ExecuteBash {
34
144
throw new Error ( 'Bash command cannot be empty.' )
35
145
}
36
146
37
- const args = ExecuteBash . parseCommand ( this . command )
147
+ const args = split ( this . command )
38
148
if ( ! args || args . length === 0 ) {
39
149
throw new Error ( 'No command found.' )
40
150
}
@@ -46,22 +156,67 @@ export class ExecuteBash {
46
156
}
47
157
}
48
158
49
- public requiresAcceptance ( ) : boolean {
159
+ public requiresAcceptance ( ) : CommandValidation {
50
160
try {
51
- const args = ExecuteBash . parseCommand ( this . command )
161
+ const args = split ( this . command )
52
162
if ( ! args || args . length === 0 ) {
53
- return true
163
+ return { requiresAcceptance : true }
54
164
}
55
165
56
- if ( args . some ( ( arg ) => dangerousPatterns . some ( ( pattern ) => arg . includes ( pattern ) ) ) ) {
57
- return true
166
+ // Split commands by pipe and process each segment
167
+ let currentCmd : string [ ] = [ ]
168
+ const allCommands : string [ ] [ ] = [ ]
169
+
170
+ for ( const arg of args ) {
171
+ if ( arg === '|' ) {
172
+ if ( currentCmd . length > 0 ) {
173
+ allCommands . push ( currentCmd )
174
+ }
175
+ currentCmd = [ ]
176
+ } else if ( arg . includes ( '|' ) ) {
177
+ return { requiresAcceptance : true }
178
+ } else {
179
+ currentCmd . push ( arg )
180
+ }
181
+ }
182
+
183
+ if ( currentCmd . length > 0 ) {
184
+ allCommands . push ( currentCmd )
58
185
}
59
186
60
- const command = args [ 0 ]
61
- return ! readOnlyCommands . includes ( command )
187
+ for ( const cmdArgs of allCommands ) {
188
+ if ( cmdArgs . length === 0 ) {
189
+ return { requiresAcceptance : true }
190
+ }
191
+
192
+ const command = cmdArgs [ 0 ]
193
+ const category = commandCategories . get ( command )
194
+
195
+ switch ( category ) {
196
+ case CommandCategory . Destructive :
197
+ return { requiresAcceptance : true , warning : destructiveCommandWarningMessage }
198
+ case CommandCategory . HighRisk :
199
+ return {
200
+ requiresAcceptance : true ,
201
+ warning : highRiskCommandWarningMessage ,
202
+ }
203
+ case CommandCategory . ReadOnly :
204
+ if (
205
+ cmdArgs . some ( ( arg ) =>
206
+ Array . from ( dangerousPatterns ) . some ( ( pattern ) => arg . includes ( pattern ) )
207
+ )
208
+ ) {
209
+ return { requiresAcceptance : true , warning : highRiskCommandWarningMessage }
210
+ }
211
+ return { requiresAcceptance : false }
212
+ default :
213
+ return { requiresAcceptance : true , warning : highRiskCommandWarningMessage }
214
+ }
215
+ }
216
+ return { requiresAcceptance : true }
62
217
} catch ( error ) {
63
218
this . logger . warn ( `Error while checking acceptance: ${ ( error as Error ) . message } ` )
64
- return true
219
+ return { requiresAcceptance : true }
65
220
}
66
221
}
67
222
@@ -167,43 +322,6 @@ export class ExecuteBash {
167
322
return output
168
323
}
169
324
170
- private static parseCommand ( command : string ) : string [ ] | undefined {
171
- const result : string [ ] = [ ]
172
- let current = ''
173
- let inQuote : string | undefined
174
- let escaped = false
175
-
176
- for ( const char of command ) {
177
- if ( escaped ) {
178
- current += char
179
- escaped = false
180
- } else if ( char === '\\' ) {
181
- escaped = true
182
- } else if ( inQuote ) {
183
- if ( char === inQuote ) {
184
- inQuote = undefined
185
- } else {
186
- current += char
187
- }
188
- } else if ( char === '"' || char === "'" ) {
189
- inQuote = char
190
- } else if ( char === ' ' || char === '\t' ) {
191
- if ( current ) {
192
- result . push ( current )
193
- current = ''
194
- }
195
- } else {
196
- current += char
197
- }
198
- }
199
-
200
- if ( current ) {
201
- result . push ( current )
202
- }
203
-
204
- return result
205
- }
206
-
207
325
public queueDescription ( updates : Writable ) : void {
208
326
updates . write ( `I will run the following shell command:\n` )
209
327
updates . write ( '```bash\n' + this . command + '\n```' )
0 commit comments