22// Provide --no-http, --no-git, --no-python to disable individually.
33// Decision: keep one-shot CLI on a current-thread runtime; reserve multi-thread
44// runtime for MCP only so cold-start work stays off the common path.
5+ // Decision: CLI uses relaxed execution limits (ExecutionLimits::cli()) because
6+ // the user explicitly chose to run the script. Counting-based limits are
7+ // effectively unlimited; timeout is removed (user has Ctrl-C). Memory-guarding
8+ // limits (function depth, AST depth, parser fuel) are kept.
9+ // MCP mode keeps the sandboxed defaults since requests come from LLM agents.
510
611//! Bashkit CLI - Command line interface for virtual bash execution
712//!
@@ -68,10 +73,22 @@ struct Args {
6873 #[ cfg_attr( feature = "realfs" , arg( long, value_name = "PATH" ) ) ]
6974 mount_rw : Vec < String > ,
7075
71- /// Maximum number of commands to execute (default: 10000)
76+ /// Maximum number of commands to execute (unlimited for CLI, 10000 for MCP )
7277 #[ arg( long) ]
7378 max_commands : Option < usize > ,
7479
80+ /// Maximum iterations for a single loop (unlimited for CLI, 10000 for MCP)
81+ #[ arg( long) ]
82+ max_loop_iterations : Option < usize > ,
83+
84+ /// Maximum total loop iterations across all loops (unlimited for CLI, 1000000 for MCP)
85+ #[ arg( long) ]
86+ max_total_loop_iterations : Option < usize > ,
87+
88+ /// Execution timeout in seconds (unlimited for CLI, 30 for MCP)
89+ #[ arg( long) ]
90+ timeout : Option < u64 > ,
91+
7592 #[ command( subcommand) ]
7693 subcommand : Option < SubCmd > ,
7794}
@@ -97,7 +114,7 @@ struct RunOutput {
97114 exit_code : i32 ,
98115}
99116
100- fn build_bash ( args : & Args ) -> bashkit:: Bash {
117+ fn build_bash ( args : & Args , mode : CliMode ) -> bashkit:: Bash {
101118 let mut builder = bashkit:: Bash :: builder ( ) ;
102119
103120 if !args. no_http {
@@ -118,8 +135,28 @@ fn build_bash(args: &Args) -> bashkit::Bash {
118135 builder = apply_real_mounts ( builder, & args. mount_ro , & args. mount_rw ) ;
119136 }
120137
121- if let Some ( max_cmds) = args. max_commands {
122- builder = builder. limits ( bashkit:: ExecutionLimits :: new ( ) . max_commands ( max_cmds) ) ;
138+ // CLI/script modes use relaxed limits; MCP keeps sandboxed defaults.
139+ let mut limits = if mode == CliMode :: Mcp {
140+ bashkit:: ExecutionLimits :: new ( )
141+ } else {
142+ bashkit:: ExecutionLimits :: cli ( )
143+ } ;
144+ if let Some ( v) = args. max_commands {
145+ limits = limits. max_commands ( v) ;
146+ }
147+ if let Some ( v) = args. max_loop_iterations {
148+ limits = limits. max_loop_iterations ( v) ;
149+ }
150+ if let Some ( v) = args. max_total_loop_iterations {
151+ limits = limits. max_total_loop_iterations ( v) ;
152+ }
153+ if let Some ( v) = args. timeout {
154+ limits = limits. timeout ( std:: time:: Duration :: from_secs ( v) ) ;
155+ }
156+ builder = builder. limits ( limits) ;
157+
158+ if mode != CliMode :: Mcp {
159+ builder = builder. session_limits ( bashkit:: SessionLimits :: unlimited ( ) ) ;
123160 }
124161
125162 builder. build ( )
@@ -184,10 +221,11 @@ fn main() -> Result<()> {
184221
185222 let args = Args :: parse ( ) ;
186223
187- match cli_mode ( & args) {
188- CliMode :: Mcp => run_mcp ( args) ,
224+ let mode = cli_mode ( & args) ;
225+ match mode {
226+ CliMode :: Mcp => run_mcp ( args, mode) ,
189227 CliMode :: Command | CliMode :: Script => {
190- let output = run_oneshot ( args) ?;
228+ let output = run_oneshot ( args, mode ) ?;
191229 print ! ( "{}" , output. stdout) ;
192230 if !output. stderr . is_empty ( ) {
193231 eprint ! ( "{}" , output. stderr) ;
@@ -202,21 +240,21 @@ fn main() -> Result<()> {
202240 }
203241}
204242
205- fn run_mcp ( args : Args ) -> Result < ( ) > {
243+ fn run_mcp ( args : Args , mode : CliMode ) -> Result < ( ) > {
206244 Builder :: new_multi_thread ( )
207245 . enable_all ( )
208246 . build ( )
209247 . context ( "Failed to build MCP runtime" ) ?
210- . block_on ( mcp:: run ( move || build_bash ( & args) ) )
248+ . block_on ( mcp:: run ( move || build_bash ( & args, mode ) ) )
211249}
212250
213- fn run_oneshot ( args : Args ) -> Result < RunOutput > {
251+ fn run_oneshot ( args : Args , mode : CliMode ) -> Result < RunOutput > {
214252 Builder :: new_current_thread ( )
215253 . enable_all ( )
216254 . build ( )
217255 . context ( "Failed to build CLI runtime" ) ?
218256 . block_on ( async move {
219- let mut bash = build_bash ( & args) ;
257+ let mut bash = build_bash ( & args, mode ) ;
220258
221259 if let Some ( cmd) = args. command {
222260 let result = bash. exec ( & cmd) . await . context ( "Failed to execute command" ) ?;
@@ -302,7 +340,7 @@ mod tests {
302340 #[ tokio:: test]
303341 async fn python_enabled_by_default ( ) {
304342 let args = Args :: parse_from ( [ "bashkit" , "-c" , "python --version" ] ) ;
305- let mut bash = build_bash ( & args) ;
343+ let mut bash = build_bash ( & args, CliMode :: Command ) ;
306344 let result = bash. exec ( "python --version" ) . await . expect ( "exec" ) ;
307345 assert_ne ! ( result. stderr, "python: command not found\n " ) ;
308346 }
@@ -311,23 +349,23 @@ mod tests {
311349 #[ tokio:: test]
312350 async fn python_can_be_disabled ( ) {
313351 let args = Args :: parse_from ( [ "bashkit" , "--no-python" , "-c" , "python --version" ] ) ;
314- let mut bash = build_bash ( & args) ;
352+ let mut bash = build_bash ( & args, CliMode :: Command ) ;
315353 let result = bash. exec ( "python --version" ) . await . expect ( "exec" ) ;
316354 assert ! ( result. stderr. contains( "command not found" ) ) ;
317355 }
318356
319357 #[ tokio:: test]
320358 async fn git_enabled_by_default ( ) {
321359 let args = Args :: parse_from ( [ "bashkit" , "-c" , "git init /repo" ] ) ;
322- let mut bash = build_bash ( & args) ;
360+ let mut bash = build_bash ( & args, CliMode :: Command ) ;
323361 let result = bash. exec ( "git init /repo" ) . await . expect ( "exec" ) ;
324362 assert_eq ! ( result. exit_code, 0 ) ;
325363 }
326364
327365 #[ tokio:: test]
328366 async fn git_can_be_disabled ( ) {
329367 let args = Args :: parse_from ( [ "bashkit" , "--no-git" , "-c" , "git init /repo" ] ) ;
330- let mut bash = build_bash ( & args) ;
368+ let mut bash = build_bash ( & args, CliMode :: Command ) ;
331369 let result = bash. exec ( "git init /repo" ) . await . expect ( "exec" ) ;
332370 assert ! ( result. stderr. contains( "not configured" ) ) ;
333371 }
@@ -336,15 +374,15 @@ mod tests {
336374 async fn http_enabled_by_default ( ) {
337375 // curl should be recognized (not "command not found") even if network fails
338376 let args = Args :: parse_from ( [ "bashkit" , "-c" , "curl --help" ] ) ;
339- let mut bash = build_bash ( & args) ;
377+ let mut bash = build_bash ( & args, CliMode :: Command ) ;
340378 let result = bash. exec ( "curl --help" ) . await . expect ( "exec" ) ;
341379 assert ! ( !result. stderr. contains( "command not found" ) ) ;
342380 }
343381
344382 #[ tokio:: test]
345383 async fn http_can_be_disabled ( ) {
346384 let args = Args :: parse_from ( [ "bashkit" , "--no-http" , "-c" , "curl https://example.com" ] ) ;
347- let mut bash = build_bash ( & args) ;
385+ let mut bash = build_bash ( & args, CliMode :: Command ) ;
348386 let result = bash. exec ( "curl https://example.com" ) . await . expect ( "exec" ) ;
349387 assert ! ( result. stderr. contains( "not configured" ) ) ;
350388 }
@@ -359,7 +397,7 @@ mod tests {
359397 "-c" ,
360398 "echo works" ,
361399 ] ) ;
362- let mut bash = build_bash ( & args) ;
400+ let mut bash = build_bash ( & args, CliMode :: Command ) ;
363401 let result = bash. exec ( "echo works" ) . await . expect ( "exec" ) ;
364402 assert_eq ! ( result. stdout, "works\n " ) ;
365403 assert_eq ! ( result. exit_code, 0 ) ;
@@ -368,7 +406,7 @@ mod tests {
368406 #[ test]
369407 fn run_oneshot_executes_command_on_current_thread_runtime ( ) {
370408 let args = Args :: parse_from ( [ "bashkit" , "--no-http" , "--no-git" , "-c" , "echo works" ] ) ;
371- let output = run_oneshot ( args) . expect ( "run" ) ;
409+ let output = run_oneshot ( args, CliMode :: Command ) . expect ( "run" ) ;
372410 assert_eq ! ( output. stdout, "works\n " ) ;
373411 assert_eq ! ( output. stderr, "" ) ;
374412 assert_eq ! ( output. exit_code, 0 ) ;
@@ -404,7 +442,7 @@ mod tests {
404442 "-c" ,
405443 "cat /mnt/data/test.txt" ,
406444 ] ) ;
407- let mut bash = build_bash ( & args) ;
445+ let mut bash = build_bash ( & args, CliMode :: Command ) ;
408446 let result = bash. exec ( "cat /mnt/data/test.txt" ) . await . expect ( "exec" ) ;
409447 assert_eq ! ( result. stdout, "from host\n " ) ;
410448 }
@@ -422,7 +460,7 @@ mod tests {
422460 "-c" ,
423461 "echo result > /mnt/out/r.txt" ,
424462 ] ) ;
425- let mut bash = build_bash ( & args) ;
463+ let mut bash = build_bash ( & args, CliMode :: Command ) ;
426464 bash. exec ( "echo result > /mnt/out/r.txt" )
427465 . await
428466 . expect ( "exec" ) ;
0 commit comments