agcodex_core/
spawn.rs

1use std::collections::HashMap;
2use std::path::PathBuf;
3use std::process::Stdio;
4use tokio::process::Child;
5use tokio::process::Command;
6use tracing::trace;
7
8use crate::protocol::SandboxPolicy;
9
10/// Experimental environment variable that will be set to some non-empty value
11/// if both of the following are true:
12///
13/// 1. The process was spawned by Codex as part of a shell tool call.
14/// 2. SandboxPolicy.has_full_network_access() was false for the tool call.
15///
16/// We may try to have just one environment variable for all sandboxing
17/// attributes, so this may change in the future.
18pub const CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR: &str = "CODEX_SANDBOX_NETWORK_DISABLED";
19
20/// Should be set when the process is spawned under a sandbox. Currently, the
21/// value is "seatbelt" for macOS, but it may change in the future to
22/// accommodate sandboxing configuration and other sandboxing mechanisms.
23pub const CODEX_SANDBOX_ENV_VAR: &str = "CODEX_SANDBOX";
24
25#[derive(Debug, Clone, Copy)]
26pub enum StdioPolicy {
27    RedirectForShellTool,
28    Inherit,
29}
30
31/// Spawns the appropriate child process for the ExecParams and SandboxPolicy,
32/// ensuring the args and environment variables used to create the `Command`
33/// (and `Child`) honor the configuration.
34///
35/// For now, we take `SandboxPolicy` as a parameter to spawn_child() because
36/// we need to determine whether to set the
37/// `CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR` environment variable.
38pub(crate) async fn spawn_child_async(
39    program: PathBuf,
40    args: Vec<String>,
41    #[cfg_attr(not(unix), allow(unused_variables))] arg0: Option<&str>,
42    cwd: PathBuf,
43    sandbox_policy: &SandboxPolicy,
44    stdio_policy: StdioPolicy,
45    env: HashMap<String, String>,
46) -> std::io::Result<Child> {
47    trace!(
48        "spawn_child_async: {program:?} {args:?} {arg0:?} {cwd:?} {sandbox_policy:?} {stdio_policy:?} {env:?}"
49    );
50
51    let mut cmd = Command::new(&program);
52    #[cfg(unix)]
53    cmd.arg0(arg0.map_or_else(|| program.to_string_lossy().to_string(), String::from));
54    cmd.args(args);
55    cmd.current_dir(cwd);
56    cmd.env_clear();
57    cmd.envs(env);
58
59    if !sandbox_policy.has_full_network_access() {
60        cmd.env(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR, "1");
61    }
62
63    // If this Codex process dies (including being killed via SIGKILL), we want
64    // any child processes that were spawned as part of a `"shell"` tool call
65    // to also be terminated.
66
67    // This relies on prctl(2), so it only works on Linux.
68    #[cfg(target_os = "linux")]
69    unsafe {
70        cmd.pre_exec(|| {
71            // This prctl call effectively requests, "deliver SIGTERM when my
72            // current parent dies."
73            if libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGTERM) == -1 {
74                return Err(std::io::Error::last_os_error());
75            }
76
77            // Though if there was a race condition and this pre_exec() block is
78            // run _after_ the parent (i.e., the Codex process) has already
79            // exited, then the parent is the _init_ process (which will never
80            // die), so we should just terminate the child process now.
81            if libc::getppid() == 1 {
82                libc::raise(libc::SIGTERM);
83            }
84            Ok(())
85        });
86    }
87
88    match stdio_policy {
89        StdioPolicy::RedirectForShellTool => {
90            // Do not create a file descriptor for stdin because otherwise some
91            // commands may hang forever waiting for input. For example, ripgrep has
92            // a heuristic where it may try to read from stdin as explained here:
93            // https://github.com/BurntSushi/ripgrep/blob/e2362d4d5185d02fa857bf381e7bd52e66fafc73/crates/core/flags/hiargs.rs#L1101-L1103
94            cmd.stdin(Stdio::null());
95
96            cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
97        }
98        StdioPolicy::Inherit => {
99            // Inherit stdin, stdout, and stderr from the parent process.
100            cmd.stdin(Stdio::inherit())
101                .stdout(Stdio::inherit())
102                .stderr(Stdio::inherit());
103        }
104    }
105
106    cmd.kill_on_drop(true).spawn()
107}