orchestrator_runner/runner/
spawn.rs1use super::policy::enforce_runner_policy;
2use super::profile::ResolvedExecutionProfile;
3use super::sandbox::{build_command_for_profile, classify_sandbox_spawn_error};
4use crate::output_capture::{OutputCaptureHandles, spawn_sanitized_output_capture};
5use anyhow::{Context, Result};
6use orchestrator_config::config::{RunnerConfig, RunnerExecutorKind, RunnerPolicy};
7use std::fs::File;
8use std::path::Path;
9use std::process::Stdio;
10
11pub struct SpawnParams<'a> {
13 pub runner: &'a RunnerConfig,
15 pub command: &'a str,
17 pub cwd: &'a Path,
19 pub stdio_mode: RunnerStdioMode,
21 pub extra_env: &'a std::collections::HashMap<String, String>,
23 pub pipe_stdin: bool,
25 pub execution_profile: &'a ResolvedExecutionProfile,
27}
28
29pub enum RunnerStdioMode {
31 Files {
33 stdout: File,
35 stderr: File,
37 },
38 Piped,
40}
41
42pub trait RunnerExecutor {
44 fn spawn(&self, params: SpawnParams<'_>) -> Result<tokio::process::Child>;
46}
47
48#[derive(Debug, Default)]
49pub struct ShellRunnerExecutor;
51
52impl RunnerExecutor for ShellRunnerExecutor {
53 fn spawn(&self, params: SpawnParams<'_>) -> Result<tokio::process::Child> {
54 let SpawnParams {
55 runner,
56 command,
57 cwd,
58 stdio_mode,
59 extra_env,
60 pipe_stdin,
61 execution_profile,
62 } = params;
63
64 enforce_runner_policy(runner, command)?;
65
66 if let Some(pid_str) = extra_env.get("ORCHESTRATOR_DAEMON_PID") {
70 if let Ok(daemon_pid) = pid_str.parse::<u32>() {
71 super::policy::guard_daemon_pid_kill(command, daemon_pid)?;
72 }
73 }
74
75 let mut cmd = build_command_for_profile(runner, command, cwd, execution_profile)?;
76
77 match stdio_mode {
78 RunnerStdioMode::Files { stdout, stderr } => {
79 cmd.stdout(Stdio::from(stdout)).stderr(Stdio::from(stderr));
80 }
81 RunnerStdioMode::Piped => {
82 cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
83 }
84 }
85
86 cmd.kill_on_drop(true);
87
88 if pipe_stdin {
89 cmd.stdin(Stdio::piped());
90 }
91
92 #[cfg(unix)]
93 {
94 use super::resource_limits::apply_unix_resource_limits_to_command;
95 cmd.process_group(0); apply_unix_resource_limits_to_command(&mut cmd, execution_profile)?;
97 }
98
99 if runner.policy == RunnerPolicy::Allowlist {
100 cmd.env_clear();
101 for key in &runner.env_allowlist {
102 if let Ok(value) = std::env::var(key) {
103 cmd.env(key, value);
104 }
105 }
106 }
107
108 for (k, v) in extra_env {
110 cmd.env(k, v);
111 }
112
113 cmd.env_remove("CLAUDECODE");
116
117 match cmd.spawn() {
118 Ok(child) => Ok(child),
119 Err(err) => {
120 if let Some(sandbox_err) = classify_sandbox_spawn_error(execution_profile, &err) {
121 return Err(sandbox_err.into());
122 }
123 Err(err).with_context(|| {
124 format!(
125 "failed to spawn runner shell={} shell_arg={}",
126 runner.shell, runner.shell_arg
127 )
128 })
129 }
130 }
131 }
132}
133
134#[allow(clippy::too_many_arguments)]
135pub fn spawn_with_runner(
137 runner: &RunnerConfig,
138 command: &str,
139 cwd: &Path,
140 stdout: File,
141 stderr: File,
142 extra_env: &std::collections::HashMap<String, String>,
143 pipe_stdin: bool,
144 execution_profile: &ResolvedExecutionProfile,
145) -> Result<tokio::process::Child> {
146 match runner.executor {
147 RunnerExecutorKind::Shell => ShellRunnerExecutor.spawn(SpawnParams {
148 runner,
149 command,
150 cwd,
151 stdio_mode: RunnerStdioMode::Files { stdout, stderr },
152 extra_env,
153 pipe_stdin,
154 execution_profile,
155 }),
156 }
157}
158
159pub struct CapturedChild {
161 pub child: tokio::process::Child,
163 pub output_capture: OutputCaptureHandles,
165}
166
167#[allow(clippy::too_many_arguments)]
168pub fn spawn_with_runner_and_capture(
170 runner: &RunnerConfig,
171 command: &str,
172 cwd: &Path,
173 stdout: File,
174 stderr: File,
175 redaction_patterns: Vec<String>,
176 extra_env: &std::collections::HashMap<String, String>,
177 pipe_stdin: bool,
178 execution_profile: &ResolvedExecutionProfile,
179) -> Result<CapturedChild> {
180 let mut child = match runner.executor {
181 RunnerExecutorKind::Shell => ShellRunnerExecutor.spawn(SpawnParams {
182 runner,
183 command,
184 cwd,
185 stdio_mode: RunnerStdioMode::Piped,
186 extra_env,
187 pipe_stdin,
188 execution_profile,
189 })?,
190 };
191 let child_stdout = child
192 .stdout
193 .take()
194 .context("captured runner child missing stdout pipe")?;
195 let child_stderr = child
196 .stderr
197 .take()
198 .context("captured runner child missing stderr pipe")?;
199 let output_capture = spawn_sanitized_output_capture(
200 child_stdout,
201 child_stderr,
202 stdout,
203 stderr,
204 redaction_patterns,
205 );
206 Ok(CapturedChild {
207 child,
208 output_capture,
209 })
210}
211
212pub async fn kill_child_process_group(child: &mut tokio::process::Child) {
219 if let Some(pid) = child.id() {
220 #[cfg(unix)]
221 {
222 unsafe {
226 libc::kill(-(pid as i32), libc::SIGKILL);
227 }
228 }
229 #[cfg(not(unix))]
230 {
231 let _ = child.kill().await;
232 }
233 } else {
234 let _ = child.kill().await;
235 }
236}