@@ -15,6 +15,10 @@ use crate::task::{ExtractedArchive, SweForgeTask};
1515
1616const MAX_OUTPUT : usize = 1024 * 1024 ;
1717
18+ fn shell_escape ( s : & str ) -> String {
19+ format ! ( "'{}'" , s. replace( '\'' , "'\\ ''" ) )
20+ }
21+
1822fn truncate_output ( raw : & [ u8 ] ) -> String {
1923 if raw. len ( ) <= MAX_OUTPUT {
2024 String :: from_utf8_lossy ( raw) . to_string ( )
@@ -64,6 +68,7 @@ async fn run_cmd(
6468 ) )
6569}
6670
71+ #[ allow( dead_code) ]
6772async fn run_shell (
6873 shell_cmd : & str ,
6974 cwd : & Path ,
@@ -73,22 +78,59 @@ async fn run_shell(
7378 run_cmd ( & [ "sh" , "-c" , shell_cmd] , cwd, timeout, env) . await
7479}
7580
76- /// Filter out system-level package commands that can't run in restricted containers.
77- /// Keeps project-level install commands (npm install, pip install, yarn install, etc.)
78- fn filter_install_command ( cmd : & str ) -> String {
79- let system_prefixes = [
80- "apt-get" , "apt " , "dpkg" , "yum " , "dnf " , "pacman " , "apk " , "snap " , "flatpak " ,
81- ] ;
81+ /// Get the agent user name from AGENT_USER env var (default: "agent").
82+ fn agent_user ( ) -> String {
83+ std:: env:: var ( "AGENT_USER" ) . unwrap_or_else ( |_| "agent" . to_string ( ) )
84+ }
85+
86+ /// Wrap a shell command to run as the agent user via sudo.
87+ fn as_agent_cmd ( shell_cmd : & str , cwd : & Path ) -> Vec < String > {
88+ let user = agent_user ( ) ;
89+ vec ! [
90+ "sudo" . to_string( ) ,
91+ "-u" . to_string( ) ,
92+ user,
93+ "--preserve-env=PATH,HOME,JAVA_HOME,GOPATH,CARGO_HOME,NODE_PATH" . to_string( ) ,
94+ "sh" . to_string( ) ,
95+ "-c" . to_string( ) ,
96+ format!( "cd {} && {}" , cwd. display( ) , shell_cmd) ,
97+ ]
98+ }
8299
83- // Split on && and filter out system commands
100+ /// Run a shell command as the agent user.
101+ async fn run_shell_as_agent (
102+ shell_cmd : & str ,
103+ cwd : & Path ,
104+ timeout : Duration ,
105+ env : Option < & [ ( & str , & str ) ] > ,
106+ ) -> Result < ( String , String , i32 ) > {
107+ let argv_owned = as_agent_cmd ( shell_cmd, cwd) ;
108+ let argv: Vec < & str > = argv_owned. iter ( ) . map ( |s| s. as_str ( ) ) . collect ( ) ;
109+ run_cmd ( & argv, cwd, timeout, env) . await
110+ }
111+
112+ /// Prepare install command: add -y flag to apt-get commands, add DEBIAN_FRONTEND=noninteractive.
113+ fn prepare_install_command ( cmd : & str ) -> String {
84114 let parts: Vec < & str > = cmd. split ( "&&" ) . collect ( ) ;
85- let filtered : Vec < & str > = parts
115+ let prepared : Vec < String > = parts
86116 . iter ( )
87- . map ( |p| p. trim ( ) )
88- . filter ( |p| !system_prefixes. iter ( ) . any ( |prefix| p. starts_with ( prefix) ) )
117+ . map ( |p| {
118+ let trimmed = p. trim ( ) ;
119+ if trimmed. starts_with ( "apt-get" ) || trimmed. starts_with ( "apt " ) {
120+ if trimmed. contains ( "install" ) && !trimmed. contains ( "-y" ) {
121+ format ! (
122+ "DEBIAN_FRONTEND=noninteractive sudo {}" ,
123+ trimmed. replace( "install" , "install -y" )
124+ )
125+ } else {
126+ format ! ( "DEBIAN_FRONTEND=noninteractive sudo {}" , trimmed)
127+ }
128+ } else {
129+ trimmed. to_string ( )
130+ }
131+ } )
89132 . collect ( ) ;
90-
91- filtered. join ( " && " )
133+ prepared. join ( " && " )
92134}
93135
94136pub struct Executor {
@@ -347,21 +389,24 @@ async fn run_task_pipeline(
347389 }
348390
349391 result. status = TaskStatus :: InstallingDeps ;
392+ // Ensure repo_dir is writable by the agent user
393+ let _ = run_cmd (
394+ & [ "chmod" , "-R" , "a+rwX" , & repo_dir. to_string_lossy ( ) ] ,
395+ work_dir,
396+ Duration :: from_secs ( 30 ) ,
397+ None ,
398+ )
399+ . await ;
350400 if let Some ( ref install_cmds) = task. workspace . install {
351401 for cmd in install_cmds {
352- // Split chained commands and filter out system package commands
353- // that can't run in a restricted container (apt-get, dpkg, etc.)
354- let effective_cmd = filter_install_command ( cmd) ;
355- if effective_cmd. is_empty ( ) {
356- info ! (
357- "[{}] Skipping system install: {}" ,
358- task. id,
359- & cmd[ ..cmd. len( ) . min( 100 ) ]
360- ) ;
361- continue ;
362- }
363- info ! ( "[{}] Installing: {}" , task. id, effective_cmd) ;
364- let ( _, stderr, exit) = run_shell (
402+ let effective_cmd = prepare_install_command ( cmd) ;
403+ info ! (
404+ "[{}] Installing (as {}): {}" ,
405+ task. id,
406+ agent_user( ) ,
407+ effective_cmd
408+ ) ;
409+ let ( _, stderr, exit) = run_shell_as_agent (
365410 & effective_cmd,
366411 & repo_dir,
367412 Duration :: from_secs ( config. clone_timeout_secs ) ,
@@ -536,20 +581,21 @@ async fn run_agent(
536581 tokio:: fs:: write ( & prompt_path, prompt) . await ?;
537582
538583 let argv_owned = agent_runner ( agent_language, & script_name) ;
539- let argv: Vec < & str > = argv_owned. iter ( ) . map ( |s| s. as_str ( ) ) . collect ( ) ;
540- info ! ( "Running agent: {:?}" , argv) ;
541-
542- let env_vars = [
543- ( "TASK_PROMPT" , prompt_path. to_string_lossy ( ) . to_string ( ) ) ,
544- ( "REPO_DIR" , repo_dir. to_string_lossy ( ) . to_string ( ) ) ,
545- ] ;
546- let env_refs: Vec < ( & str , & str ) > = env_vars. iter ( ) . map ( |( k, v) | ( * k, v. as_str ( ) ) ) . collect ( ) ;
547-
548- let ( stdout, stderr, exit) = run_cmd (
549- & argv,
584+ let runner_cmd = argv_owned. join ( " " ) ;
585+ info ! ( "Running agent as {}: {}" , agent_user( ) , runner_cmd) ;
586+
587+ let env_export = format ! (
588+ "export TASK_PROMPT={} REPO_DIR={} && {}" ,
589+ shell_escape( & prompt_path. to_string_lossy( ) ) ,
590+ shell_escape( & repo_dir. to_string_lossy( ) ) ,
591+ runner_cmd,
592+ ) ;
593+
594+ let ( stdout, stderr, exit) = run_shell_as_agent (
595+ & env_export,
550596 repo_dir,
551597 Duration :: from_secs ( timeout_secs) ,
552- Some ( & env_refs ) ,
598+ None ,
553599 )
554600 . await ?;
555601
@@ -581,14 +627,10 @@ async fn run_tests(
581627 let _ = std:: fs:: set_permissions ( & script_path, perms) ;
582628 }
583629
584- debug ! ( "Running test script: {}" , name) ;
585- let result = run_cmd (
586- & [ "bash" , & script_path. to_string_lossy ( ) ] ,
587- repo_dir,
588- Duration :: from_secs ( timeout_secs) ,
589- None ,
590- )
591- . await ;
630+ debug ! ( "Running test script as {}: {}" , agent_user( ) , name) ;
631+ let test_cmd = format ! ( "bash {}" , shell_escape( & script_path. to_string_lossy( ) ) ) ;
632+ let result =
633+ run_shell_as_agent ( & test_cmd, repo_dir, Duration :: from_secs ( timeout_secs) , None ) . await ;
592634
593635 match result {
594636 Ok ( ( stdout, stderr, exit) ) => {
0 commit comments