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