Skip to main content

codex/commands/
sandbox.rs

1use tokio::{process::Command, time};
2
3use crate::{
4    process::{spawn_with_retry, tee_stream, ConsoleTarget},
5    CodexClient, CodexError, SandboxCommandRequest, SandboxPlatform, SandboxRun, StdioToUdsRequest,
6};
7
8impl CodexClient {
9    /// Spawns `codex stdio-to-uds <SOCKET_PATH>` with piped stdio for manual relays.
10    ///
11    /// Returns the child process so callers can write to stdin/read from stdout (e.g., to bridge a
12    /// JSON-RPC transport over a Unix domain socket). Fails fast on empty socket paths and inherits
13    /// the builder working directory when none is provided on the request.
14    pub fn stdio_to_uds(
15        &self,
16        request: StdioToUdsRequest,
17    ) -> Result<tokio::process::Child, CodexError> {
18        let StdioToUdsRequest {
19            socket_path,
20            working_dir,
21        } = request;
22
23        if socket_path.as_os_str().is_empty() {
24            return Err(CodexError::EmptySocketPath);
25        }
26
27        let mut command = Command::new(self.command_env.binary_path());
28        command
29            .arg("stdio-to-uds")
30            .arg(&socket_path)
31            .stdin(std::process::Stdio::piped())
32            .stdout(std::process::Stdio::piped())
33            .stderr(std::process::Stdio::piped())
34            .kill_on_drop(true)
35            .current_dir(self.sandbox_working_dir(working_dir)?);
36
37        self.command_env.apply(&mut command)?;
38
39        spawn_with_retry(&mut command, self.command_env.binary_path())
40    }
41
42    /// Runs `codex sandbox <platform> [--full-auto|--log-denials] [--config/--enable/--disable] -- <COMMAND...>`.
43    ///
44    /// Captures stdout/stderr and mirrors them according to the builder (`mirror_stdout` / `quiet`). Unlike
45    /// `apply`/`diff`, non-zero exit codes are returned in [`SandboxRun::status`] without being wrapped in
46    /// [`CodexError::NonZeroExit`]. macOS denial logging is enabled via [`SandboxCommandRequest::log_denials`]
47    /// and ignored on other platforms. Linux uses the bundled `codex-linux-sandbox` helper; Windows sandboxing
48    /// is experimental and relies on the upstream helper. The wrapper does not gate availability—unsupported
49    /// installs will surface as non-zero statuses.
50    pub async fn run_sandbox(
51        &self,
52        request: SandboxCommandRequest,
53    ) -> Result<SandboxRun, CodexError> {
54        if request.command.is_empty() {
55            return Err(CodexError::EmptySandboxCommand);
56        }
57
58        let SandboxCommandRequest {
59            platform,
60            command,
61            full_auto,
62            log_denials,
63            allow_unix_socket,
64            config_overrides,
65            feature_toggles,
66            working_dir,
67        } = request;
68
69        let working_dir = self.sandbox_working_dir(working_dir)?;
70
71        let mut process = Command::new(self.command_env.binary_path());
72        process
73            .arg("sandbox")
74            .arg(platform.subcommand())
75            .stdout(std::process::Stdio::piped())
76            .stderr(std::process::Stdio::piped())
77            .kill_on_drop(true)
78            .current_dir(&working_dir);
79
80        if full_auto {
81            process.arg("--full-auto");
82        }
83
84        if log_denials && matches!(platform, SandboxPlatform::Macos) {
85            process.arg("--log-denials");
86        }
87
88        if allow_unix_socket && matches!(platform, SandboxPlatform::Macos) {
89            process.arg("--allow-unix-socket");
90        }
91
92        for override_ in config_overrides {
93            process.arg("--config");
94            process.arg(format!("{}={}", override_.key, override_.value));
95        }
96
97        for feature in feature_toggles.enable {
98            process.arg("--enable");
99            process.arg(feature);
100        }
101
102        for feature in feature_toggles.disable {
103            process.arg("--disable");
104            process.arg(feature);
105        }
106
107        process.arg("--");
108        process.args(&command);
109
110        self.command_env.apply(&mut process)?;
111
112        let mut child = spawn_with_retry(&mut process, self.command_env.binary_path())?;
113
114        let stdout = child.stdout.take().ok_or(CodexError::StdoutUnavailable)?;
115        let stderr = child.stderr.take().ok_or(CodexError::StderrUnavailable)?;
116
117        let stdout_task = tokio::spawn(tee_stream(
118            stdout,
119            ConsoleTarget::Stdout,
120            self.mirror_stdout,
121        ));
122        let stderr_task = tokio::spawn(tee_stream(stderr, ConsoleTarget::Stderr, !self.quiet));
123
124        let wait_task = async move {
125            let status = child
126                .wait()
127                .await
128                .map_err(|source| CodexError::Wait { source })?;
129            let stdout_bytes = stdout_task
130                .await
131                .map_err(CodexError::Join)?
132                .map_err(CodexError::CaptureIo)?;
133            let stderr_bytes = stderr_task
134                .await
135                .map_err(CodexError::Join)?
136                .map_err(CodexError::CaptureIo)?;
137            Ok::<_, CodexError>((status, stdout_bytes, stderr_bytes))
138        };
139
140        let (status, stdout_bytes, stderr_bytes) = if self.timeout.is_zero() {
141            wait_task.await?
142        } else {
143            match time::timeout(self.timeout, wait_task).await {
144                Ok(result) => result?,
145                Err(_) => {
146                    return Err(CodexError::Timeout {
147                        timeout: self.timeout,
148                    });
149                }
150            }
151        };
152
153        Ok(SandboxRun {
154            status,
155            stdout: String::from_utf8(stdout_bytes)?,
156            stderr: String::from_utf8(stderr_bytes)?,
157        })
158    }
159}