| 402 | } |
| 403 | |
| 404 | async fn run_command(&self, input: &serde_json::Value) -> AppResult<String> { |
| 405 | let cmd = input["command"] |
| 406 | .as_str() |
| 407 | .ok_or_else(|| AppError::Validation("Missing 'command' field".to_string()))?; |
| 408 | |
| 409 | // Platform-specific shell selection |
| 410 | let mut command = if cfg!(target_os = "windows") { |
| 411 | let mut cmd_process = Command::new("cmd"); |
| 412 | cmd_process.arg("/C").arg(cmd); |
| 413 | cmd_process |
| 414 | } else { |
| 415 | let mut sh_process = Command::new("sh"); |
| 416 | sh_process.arg("-c").arg(cmd); |
| 417 | sh_process |
| 418 | }; |
| 419 | |
| 420 | let safe_path = std::env::var("PATH") |
| 421 | .unwrap_or_else(|_| "/usr/local/bin:/usr/bin:/bin".to_string()); |
| 422 | |
| 423 | command |
| 424 | .current_dir(&self.sandbox) |
| 425 | .env_clear() |
| 426 | .env("PATH", safe_path) |
| 427 | .env("LANG", std::env::var("LANG").unwrap_or_else(|_| "en_US.UTF-8".to_string())) |
| 428 | .env("LC_ALL", std::env::var("LC_ALL").unwrap_or_else(|_| "en_US.UTF-8".to_string())) |
| 429 | .stdout(Stdio::piped()) |
| 430 | .stderr(Stdio::piped()) |
| 431 | .kill_on_drop(true); |
| 432 | |
| 433 | // On Unix, place the child in its own process group so we can kill any |
| 434 | // descendants the shell backgrounds. Without this, a command like |
| 435 | // `sh -c "sleep 60 &"` orphans the sleep when sh exits or is killed — |
| 436 | // it survives the timeout and continues to run with the agent's privileges. |
| 437 | #[cfg(unix)] |
| 438 | command.process_group(0); |
| 439 | |
| 440 | const MAX_OUTPUT_BYTES: usize = 256 * 1024; // 256KB limit |
| 441 | |
| 442 | let mut child = command.spawn().map_err(AppError::from)?; |
| 443 | |
| 444 | // Capture the leader pid before waiting. This is the process group ID |
| 445 | // since we requested process_group(0). |
| 446 | #[cfg(unix)] |
| 447 | let pgid = child.id().map(|id| id as i32); |
| 448 | |
| 449 | let stdout_pipe = child.stdout.take(); |
| 450 | let stderr_pipe = child.stderr.take(); |
| 451 | |
| 452 | let stdout_task = tokio::spawn(async move { |
| 453 | let mut buf = Vec::new(); |
| 454 | let mut total = 0usize; |
| 455 | if let Some(mut out) = stdout_pipe { |
| 456 | let mut chunk = [0u8; 8192]; |
| 457 | loop { |
| 458 | let n = tokio::io::AsyncReadExt::read(&mut out, &mut chunk).await?; |
| 459 | if n == 0 { |
| 460 | break; |
| 461 | } |