Skip to main content

lha_cli/
debug_sandbox.rs

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    // In practice, this should be `std::env::current_dir()` because this CLI
125    // does not support `--cwd`, but let's use the config value for consistency.
126    let cwd = config.cwd.clone();
127    // For now, we always use the same cwd for both the command and the
128    // sandbox policy. In the future, we could add a CLI option to set them
129    // separately.
130    let sandbox_policy_cwd = cwd.clone();
131
132    let stdio_policy = StdioPolicy::Inherit;
133    let env = create_env(&config.shell_environment_policy);
134
135    // Special-case Windows sandbox: execute and exit the process to emulate inherited stdio.
136    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            // Preflight audit is invoked elsewhere at the appropriate times.
157            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}