1#[cfg(target_os = "macos")]
2mod pid_tracker;
3#[cfg(target_os = "macos")]
4mod seatbelt;
5
6use std::path::PathBuf;
7
8use crate::product::agent::config::Config;
9use crate::product::agent::config::ConfigOverrides;
10use crate::product::agent::exec_env::create_env;
11use crate::product::agent::landlock::spawn_command_under_linux_sandbox;
12#[cfg(target_os = "macos")]
13use crate::product::agent::seatbelt::spawn_command_under_seatbelt;
14use crate::product::agent::spawn::StdioPolicy;
15use crate::product::common::CliConfigOverrides;
16use crate::product::protocol::config_types::SandboxMode;
17
18use crate::LandlockCommand;
19use crate::SeatbeltCommand;
20use crate::WindowsCommand;
21use crate::exit_status::handle_exit_status;
22
23#[cfg(target_os = "macos")]
24use seatbelt::DenialLogger;
25
26#[cfg(target_os = "macos")]
27pub async fn run_command_under_seatbelt(
28 command: SeatbeltCommand,
29 codex_linux_sandbox_exe: Option<PathBuf>,
30) -> anyhow::Result<()> {
31 let SeatbeltCommand {
32 full_auto,
33 log_denials,
34 config_overrides,
35 command,
36 } = command;
37 run_command_under_sandbox(
38 full_auto,
39 command,
40 config_overrides,
41 codex_linux_sandbox_exe,
42 SandboxType::Seatbelt,
43 log_denials,
44 )
45 .await
46}
47
48#[cfg(not(target_os = "macos"))]
49pub async fn run_command_under_seatbelt(
50 _command: SeatbeltCommand,
51 _codex_linux_sandbox_exe: Option<PathBuf>,
52) -> anyhow::Result<()> {
53 anyhow::bail!("Seatbelt sandbox is only available on macOS");
54}
55
56pub async fn run_command_under_landlock(
57 command: LandlockCommand,
58 codex_linux_sandbox_exe: Option<PathBuf>,
59) -> anyhow::Result<()> {
60 let LandlockCommand {
61 full_auto,
62 config_overrides,
63 command,
64 } = command;
65 run_command_under_sandbox(
66 full_auto,
67 command,
68 config_overrides,
69 codex_linux_sandbox_exe,
70 SandboxType::Landlock,
71 false,
72 )
73 .await
74}
75
76pub async fn run_command_under_windows(
77 command: WindowsCommand,
78 codex_linux_sandbox_exe: Option<PathBuf>,
79) -> anyhow::Result<()> {
80 let WindowsCommand {
81 full_auto,
82 config_overrides,
83 command,
84 } = command;
85 run_command_under_sandbox(
86 full_auto,
87 command,
88 config_overrides,
89 codex_linux_sandbox_exe,
90 SandboxType::Windows,
91 false,
92 )
93 .await
94}
95
96enum SandboxType {
97 #[cfg(target_os = "macos")]
98 Seatbelt,
99 Landlock,
100 Windows,
101}
102
103async fn run_command_under_sandbox(
104 full_auto: bool,
105 command: Vec<String>,
106 config_overrides: CliConfigOverrides,
107 codex_linux_sandbox_exe: Option<PathBuf>,
108 sandbox_type: SandboxType,
109 log_denials: bool,
110) -> anyhow::Result<()> {
111 let sandbox_mode = create_sandbox_mode(full_auto);
112 let config = Config::load_with_cli_overrides_and_harness_overrides(
113 config_overrides
114 .parse_overrides()
115 .map_err(anyhow::Error::msg)?,
116 ConfigOverrides {
117 sandbox_mode: Some(sandbox_mode),
118 codex_linux_sandbox_exe,
119 ..Default::default()
120 },
121 )
122 .await?;
123
124 let cwd = config.cwd.clone();
127 let sandbox_policy_cwd = cwd.clone();
131
132 let stdio_policy = StdioPolicy::Inherit;
133 let env = create_env(&config.shell_environment_policy);
134
135 if let SandboxType::Windows = sandbox_type {
137 #[cfg(target_os = "windows")]
138 {
139 use crate::product::agent::windows_sandbox::WindowsSandboxLevelExt;
140 use crate::product::protocol::config_types::WindowsSandboxLevel;
141 use crate::product::windows_sandbox::run_windows_sandbox_capture;
142 use crate::product::windows_sandbox::run_windows_sandbox_capture_elevated;
143
144 let policy_str = serde_json::to_string(config.sandbox_policy.get())?;
145
146 let sandbox_cwd = sandbox_policy_cwd.clone();
147 let cwd_clone = cwd.clone();
148 let env_map = env.clone();
149 let command_vec = command.clone();
150 let base_dir = config.lha_home.clone();
151 let use_elevated = matches!(
152 WindowsSandboxLevel::from_config(&config),
153 WindowsSandboxLevel::Elevated
154 );
155
156 let res = tokio::task::spawn_blocking(move || {
158 if use_elevated {
159 run_windows_sandbox_capture_elevated(
160 policy_str.as_str(),
161 &sandbox_cwd,
162 base_dir.as_path(),
163 command_vec,
164 &cwd_clone,
165 env_map,
166 None,
167 )
168 } else {
169 run_windows_sandbox_capture(
170 policy_str.as_str(),
171 &sandbox_cwd,
172 base_dir.as_path(),
173 command_vec,
174 &cwd_clone,
175 env_map,
176 None,
177 )
178 }
179 })
180 .await;
181
182 let capture = match res {
183 Ok(Ok(v)) => v,
184 Ok(Err(err)) => {
185 eprintln!("windows sandbox failed: {err}");
186 std::process::exit(1);
187 }
188 Err(join_err) => {
189 eprintln!("windows sandbox join error: {join_err}");
190 std::process::exit(1);
191 }
192 };
193
194 if !capture.stdout.is_empty() {
195 use std::io::Write;
196 let _ = std::io::stdout().write_all(&capture.stdout);
197 }
198 if !capture.stderr.is_empty() {
199 use std::io::Write;
200 let _ = std::io::stderr().write_all(&capture.stderr);
201 }
202
203 std::process::exit(capture.exit_code);
204 }
205 #[cfg(not(target_os = "windows"))]
206 {
207 anyhow::bail!("Windows sandbox is only available on Windows");
208 }
209 }
210
211 #[cfg(target_os = "macos")]
212 let mut denial_logger = log_denials.then(DenialLogger::new).flatten();
213 #[cfg(not(target_os = "macos"))]
214 let _ = log_denials;
215
216 let mut child = match sandbox_type {
217 #[cfg(target_os = "macos")]
218 SandboxType::Seatbelt => {
219 spawn_command_under_seatbelt(
220 command,
221 cwd,
222 config.sandbox_policy.get(),
223 sandbox_policy_cwd.as_path(),
224 stdio_policy,
225 env,
226 )
227 .await?
228 }
229 SandboxType::Landlock => {
230 #[expect(clippy::expect_used)]
231 let codex_linux_sandbox_exe = config
232 .codex_linux_sandbox_exe
233 .expect("lha-linux-sandbox executable not found");
234 spawn_command_under_linux_sandbox(
235 codex_linux_sandbox_exe,
236 command,
237 cwd,
238 config.sandbox_policy.get(),
239 sandbox_policy_cwd.as_path(),
240 stdio_policy,
241 env,
242 )
243 .await?
244 }
245 SandboxType::Windows => {
246 unreachable!("Windows sandbox should have been handled above");
247 }
248 };
249
250 #[cfg(target_os = "macos")]
251 if let Some(denial_logger) = &mut denial_logger {
252 denial_logger.on_child_spawn(&child);
253 }
254
255 let status = child.wait().await?;
256
257 #[cfg(target_os = "macos")]
258 if let Some(denial_logger) = denial_logger {
259 let denials = denial_logger.finish().await;
260 eprintln!("\n=== Sandbox denials ===");
261 if denials.is_empty() {
262 eprintln!("None found.");
263 } else {
264 for seatbelt::SandboxDenial { name, capability } in denials {
265 eprintln!("({name}) {capability}");
266 }
267 }
268 }
269
270 handle_exit_status(status);
271}
272
273pub fn create_sandbox_mode(full_auto: bool) -> SandboxMode {
274 if full_auto {
275 SandboxMode::WorkspaceWrite
276 } else {
277 SandboxMode::ReadOnly
278 }
279}