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            config_overrides,
64            feature_toggles,
65            working_dir,
66        } = request;
67
68        let working_dir = self.sandbox_working_dir(working_dir)?;
69
70        let mut process = Command::new(self.command_env.binary_path());
71        process
72            .arg("sandbox")
73            .arg(platform.subcommand())
74            .stdout(std::process::Stdio::piped())
75            .stderr(std::process::Stdio::piped())
76            .kill_on_drop(true)
77            .current_dir(&working_dir);
78
79        if full_auto {
80            process.arg("--full-auto");
81        }
82
83        if log_denials && matches!(platform, SandboxPlatform::Macos) {
84            process.arg("--log-denials");
85        }
86
87        for override_ in config_overrides {
88            process.arg("--config");
89            process.arg(format!("{}={}", override_.key, override_.value));
90        }
91
92        for feature in feature_toggles.enable {
93            process.arg("--enable");
94            process.arg(feature);
95        }
96
97        for feature in feature_toggles.disable {
98            process.arg("--disable");
99            process.arg(feature);
100        }
101
102        process.arg("--");
103        process.args(&command);
104
105        self.command_env.apply(&mut process)?;
106
107        let mut child = spawn_with_retry(&mut process, self.command_env.binary_path())?;
108
109        let stdout = child.stdout.take().ok_or(CodexError::StdoutUnavailable)?;
110        let stderr = child.stderr.take().ok_or(CodexError::StderrUnavailable)?;
111
112        let stdout_task = tokio::spawn(tee_stream(
113            stdout,
114            ConsoleTarget::Stdout,
115            self.mirror_stdout,
116        ));
117        let stderr_task = tokio::spawn(tee_stream(stderr, ConsoleTarget::Stderr, !self.quiet));
118
119        let wait_task = async move {
120            let status = child
121                .wait()
122                .await
123                .map_err(|source| CodexError::Wait { source })?;
124            let stdout_bytes = stdout_task
125                .await
126                .map_err(CodexError::Join)?
127                .map_err(CodexError::CaptureIo)?;
128            let stderr_bytes = stderr_task
129                .await
130                .map_err(CodexError::Join)?
131                .map_err(CodexError::CaptureIo)?;
132            Ok::<_, CodexError>((status, stdout_bytes, stderr_bytes))
133        };
134
135        let (status, stdout_bytes, stderr_bytes) = if self.timeout.is_zero() {
136            wait_task.await?
137        } else {
138            match time::timeout(self.timeout, wait_task).await {
139                Ok(result) => result?,
140                Err(_) => {
141                    return Err(CodexError::Timeout {
142                        timeout: self.timeout,
143                    });
144                }
145            }
146        };
147
148        Ok(SandboxRun {
149            status,
150            stdout: String::from_utf8(stdout_bytes)?,
151            stderr: String::from_utf8(stderr_bytes)?,
152        })
153    }
154}