@@ -24,12 +24,34 @@ use super::{
2424} ;
2525use crate :: cli:: chat:: truncate_safe;
2626
27+ const READONLY_COMMANDS : & [ & str ] = & [ "ls" , "cat" , "echo" , "pwd" , "which" , "head" , "tail" ] ;
28+
2729#[ derive( Debug , Clone , Deserialize ) ]
2830pub struct ExecuteBash {
2931 pub command : String ,
3032}
3133
3234impl ExecuteBash {
35+ pub fn requires_consent ( & self ) -> bool {
36+ let Some ( args) = shlex:: split ( & self . command ) else {
37+ return true ;
38+ } ;
39+
40+ const DANGEROUS_PATTERNS : & [ & str ] = & [ "|" , "<(" , "$(" , "`" , ">" , "&&" , "||" ] ;
41+ if args
42+ . iter ( )
43+ . any ( |arg| DANGEROUS_PATTERNS . iter ( ) . any ( |p| arg. contains ( p) ) )
44+ {
45+ return true ;
46+ }
47+
48+ if let Some ( cmd) = args. first ( ) {
49+ !READONLY_COMMANDS . contains ( & cmd. as_str ( ) )
50+ } else {
51+ true
52+ }
53+ }
54+
3355 pub async fn invoke ( & self , mut updates : impl Write ) -> Result < InvokeOutput > {
3456 // We need to maintain a handle on stderr and stdout, but pipe it to the terminal as well
3557 let mut child = tokio:: process:: Command :: new ( "bash" )
@@ -171,7 +193,6 @@ mod tests {
171193 // Verifying stderr
172194 let v = serde_json:: json!( {
173195 "command" : "echo Hello, world! 1>&2" ,
174- "interactive" : false
175196 } ) ;
176197 let out = serde_json:: from_value :: < ExecuteBash > ( v)
177198 . unwrap ( )
@@ -205,4 +226,45 @@ mod tests {
205226 panic ! ( "Expected JSON output" ) ;
206227 }
207228 }
229+
230+ #[ test]
231+ fn test_requires_consent_for_readonly_commands ( ) {
232+ let cmds = & [
233+ // Safe commands
234+ ( "ls ~" , false ) ,
235+ ( "ls -al ~" , false ) ,
236+ ( "pwd" , false ) ,
237+ ( "echo 'Hello, world!'" , false ) ,
238+ ( "which aws" , false ) ,
239+ // Potentially dangerous readonly commands
240+ ( "echo hi > myimportantfile" , true ) ,
241+ ( "ls -al >myimportantfile" , true ) ,
242+ ( "echo hi 2> myimportantfile" , true ) ,
243+ ( "echo hi >> myimportantfile" , true ) ,
244+ ( "echo $(rm myimportantfile)" , true ) ,
245+ ( "echo `rm myimportantfile`" , true ) ,
246+ ( "echo hello && rm myimportantfile" , true ) ,
247+ ( "echo hello&&rm myimportantfile" , true ) ,
248+ ( "ls nonexistantpath || rm myimportantfile" , true ) ,
249+ ( "echo myimportantfile | xargs rm" , true ) ,
250+ ( "echo myimportantfile|args rm" , true ) ,
251+ ( "echo <(rm myimportantfile)" , true ) ,
252+ ( "cat <<< 'some string here' > myimportantfile" , true ) ,
253+ ( "echo '\n #!/usr/bin/env bash\n echo hello\n ' > myscript.sh" , true ) ,
254+ ( "cat <<EOF > myimportantfile\n hello world\n EOF" , true ) ,
255+ ] ;
256+ for ( cmd, expected) in cmds {
257+ let tool = serde_json:: from_value :: < ExecuteBash > ( serde_json:: json!( {
258+ "command" : cmd,
259+ } ) )
260+ . unwrap ( ) ;
261+ assert_eq ! (
262+ tool. requires_consent( ) ,
263+ * expected,
264+ "expected command: `{}` to have requires_consent: `{}`" ,
265+ cmd,
266+ expected
267+ ) ;
268+ }
269+ }
208270}
0 commit comments