Skip to main content

codex_arg0/
lib.rs

1use std::ffi::OsString;
2use std::fs::File;
3use std::future::Future;
4use std::path::Path;
5use std::path::PathBuf;
6
7use codex_apply_patch::CODEX_CORE_APPLY_PATCH_ARG1;
8#[cfg(unix)]
9use codex_exec_server::CODEX_ARG0_EXEC_HELPER_ARG1;
10use codex_exec_server::CODEX_FS_HELPER_ARG1;
11use codex_install_context::InstallContext;
12use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0;
13use codex_utils_home_dir::find_codex_home;
14#[cfg(target_os = "windows")]
15use codex_windows_sandbox::CODEX_WINDOWS_SANDBOX_ARG1;
16#[cfg(unix)]
17use std::os::unix::fs::symlink;
18use tempfile::TempDir;
19
20const APPLY_PATCH_ARG0: &str = "apply_patch";
21const MISSPELLED_APPLY_PATCH_ARG0: &str = "applypatch";
22#[cfg(unix)]
23const EXECVE_WRAPPER_ARG0: &str = "codex-execve-wrapper";
24const LOCK_FILENAME: &str = ".lock";
25const TOKIO_WORKER_STACK_SIZE_BYTES: usize = 16 * 1024 * 1024;
26
27#[derive(Clone, Debug, Default, Eq, PartialEq)]
28pub struct Arg0DispatchPaths {
29    /// Stable path to the current Codex executable for child re-execs.
30    ///
31    /// Prefer this over [`std::env::current_exe()`] in code that may run under
32    /// a test harness, where `current_exe()` can point at the harness binary
33    /// instead of the real Codex CLI.
34    pub codex_self_exe: Option<PathBuf>,
35    pub codex_linux_sandbox_exe: Option<PathBuf>,
36    pub main_execve_wrapper_exe: Option<PathBuf>,
37}
38
39/// Keeps the per-session PATH entry alive and locked for the process lifetime.
40pub struct Arg0PathEntryGuard {
41    _temp_dir: TempDir,
42    _lock_file: File,
43    paths: Arg0DispatchPaths,
44}
45
46impl Arg0PathEntryGuard {
47    fn new(temp_dir: TempDir, lock_file: File, paths: Arg0DispatchPaths) -> Self {
48        Self {
49            _temp_dir: temp_dir,
50            _lock_file: lock_file,
51            paths,
52        }
53    }
54
55    pub fn paths(&self) -> &Arg0DispatchPaths {
56        &self.paths
57    }
58}
59
60pub fn arg0_dispatch() -> Option<Arg0PathEntryGuard> {
61    // Determine if we were invoked via the special alias.
62    let mut args = std::env::args_os();
63    let argv0 = args.next().unwrap_or_default();
64    let exe_name = Path::new(&argv0)
65        .file_name()
66        .and_then(|s| s.to_str())
67        .unwrap_or("");
68
69    #[cfg(unix)]
70    if exe_name == EXECVE_WRAPPER_ARG0 {
71        let mut args = std::env::args();
72        let _ = args.next();
73        let file = match args.next() {
74            Some(file) => file,
75            None => std::process::exit(1),
76        };
77        let argv = args.collect::<Vec<_>>();
78
79        let runtime = match tokio::runtime::Builder::new_current_thread()
80            .enable_all()
81            .build()
82        {
83            Ok(runtime) => runtime,
84            Err(_) => std::process::exit(1),
85        };
86        let exit_code = runtime.block_on(
87            codex_shell_escalation::run_shell_escalation_execve_wrapper(file, argv),
88        );
89        match exit_code {
90            Ok(exit_code) => std::process::exit(exit_code),
91            Err(_) => std::process::exit(1),
92        }
93    }
94
95    if exe_name == CODEX_LINUX_SANDBOX_ARG0 {
96        // Safety: [`run_main`] never returns.
97        codex_linux_sandbox::run_main();
98    } else if exe_name == APPLY_PATCH_ARG0 || exe_name == MISSPELLED_APPLY_PATCH_ARG0 {
99        codex_apply_patch::main();
100    }
101
102    let argv1 = args.next().unwrap_or_default();
103    #[cfg(unix)]
104    if argv1 == CODEX_ARG0_EXEC_HELPER_ARG1 {
105        codex_exec_server::run_arg0_exec_helper_main();
106    }
107    if argv1 == CODEX_FS_HELPER_ARG1 {
108        codex_exec_server::run_fs_helper_main();
109    }
110    #[cfg(target_os = "windows")]
111    if argv1 == CODEX_WINDOWS_SANDBOX_ARG1 {
112        codex_windows_sandbox::run_windows_sandbox_wrapper_main();
113    }
114    if argv1 == CODEX_CORE_APPLY_PATCH_ARG1 {
115        let patch_arg = args.next().and_then(|s| s.to_str().map(str::to_owned));
116        let exit_code = match patch_arg {
117            Some(patch_arg) => {
118                let mut stdout = std::io::stdout();
119                let mut stderr = std::io::stderr();
120                let cwd = match codex_utils_absolute_path::AbsolutePathBuf::current_dir() {
121                    Ok(cwd) => cwd,
122                    Err(_) => std::process::exit(1),
123                };
124                let runtime = match tokio::runtime::Builder::new_current_thread()
125                    .enable_all()
126                    .build()
127                {
128                    Ok(runtime) => runtime,
129                    Err(_) => std::process::exit(1),
130                };
131                let cwd = cwd.into();
132                match runtime.block_on(codex_apply_patch::apply_patch(
133                    &patch_arg,
134                    &cwd,
135                    &mut stdout,
136                    &mut stderr,
137                    codex_exec_server::LOCAL_FS.as_ref(),
138                    /*sandbox*/ None,
139                )) {
140                    Ok(_) => 0,
141                    Err(_) => 1,
142                }
143            }
144            None => {
145                eprintln!("Error: {CODEX_CORE_APPLY_PATCH_ARG1} requires a UTF-8 PATCH argument.");
146                1
147            }
148        };
149        std::process::exit(exit_code);
150    }
151
152    // This modifies the environment, which is not thread-safe, so do this
153    // before creating any threads/the Tokio runtime.
154    load_dotenv();
155
156    let (path_entry_guard, updated_path_env_var) = prepare_path_env_var_with_aliases(
157        InstallContext::current(),
158        std::env::var_os("PATH"),
159        prepare_path_entry_for_codex_aliases,
160    );
161    if let Some(updated_path_env_var) = updated_path_env_var {
162        // It is safe to call set_var() because our process is single-threaded at
163        // this point in its execution.
164        unsafe {
165            std::env::set_var("PATH", updated_path_env_var);
166        }
167    }
168    path_entry_guard
169}
170
171fn prepare_path_env_var_with_aliases(
172    install_context: &InstallContext,
173    existing_path: Option<OsString>,
174    prepare_aliases: impl FnOnce(Option<OsString>) -> std::io::Result<(Arg0PathEntryGuard, OsString)>,
175) -> (Option<Arg0PathEntryGuard>, Option<OsString>) {
176    let package_path = path_env_with_package_path_dir(install_context, existing_path.clone());
177    let path_for_aliases = package_path.clone().or(existing_path);
178
179    match prepare_aliases(path_for_aliases) {
180        Ok((path_entry, updated_path_env_var)) => (Some(path_entry), Some(updated_path_env_var)),
181        Err(err) => {
182            // It is possible that Codex will proceed successfully even if
183            // creating helper aliases fails, so warn the user and move on.
184            eprintln!("WARNING: proceeding, even though we could not create PATH aliases: {err}");
185            (None, package_path)
186        }
187    }
188}
189
190/// While we want to deploy the Codex CLI as a single executable for simplicity,
191/// we also want to expose some of its functionality as distinct CLIs, so we use
192/// the "arg0 trick" to determine which CLI to dispatch. This effectively allows
193/// us to simulate deploying multiple executables as a single binary on Mac and
194/// Linux (but not Windows).
195///
196/// When the current executable is invoked through the hard-link or alias named
197/// `codex-linux-sandbox` we *directly* execute
198/// [`codex_linux_sandbox::run_main`] (which never returns). Otherwise we:
199///
200/// 1.  Load `.env` values from `~/.codex/.env` before creating any threads.
201/// 2.  Spawn a main runtime thread with a controlled stack size.
202/// 3.  Construct a Tokio multi-thread runtime.
203/// 4.  Capture the current executable path and derive the
204///     `codex-linux-sandbox` helper path (falling back to the current
205///     executable if needed) so children can re-invoke the sandbox when running
206///     on Linux.
207/// 5.  Execute the provided async `main_fn` inside that runtime, forwarding any
208///     error. Note that `main_fn` receives [`Arg0DispatchPaths`], which
209///     contains the helper executable paths needed to construct
210///     [`codex_core::config::Config`].
211///
212/// This function should be used to wrap any `main()` function in binary crates
213/// in this workspace that depends on these helper CLIs.
214pub fn arg0_dispatch_or_else<F, Fut>(main_fn: F) -> anyhow::Result<()>
215where
216    F: FnOnce(Arg0DispatchPaths) -> Fut + Send + 'static,
217    Fut: Future<Output = anyhow::Result<()>>,
218{
219    // Retain the TempDir so it exists for the lifetime of the invocation of
220    // this executable. Admittedly, we could invoke `keep()` on it, but it
221    // would be nice to avoid leaving temporary directories behind, if possible.
222    let path_entry_guard = arg0_dispatch();
223    let current_exe = std::env::current_exe().ok();
224
225    // Regular invocation. Run the async entry point on a thread with the same
226    // stack budget as Tokio workers; `Runtime::block_on` otherwise runs the
227    // top-level future on the caller's OS stack.
228    let handle = std::thread::Builder::new()
229        .name("codex-main".to_string())
230        .stack_size(TOKIO_WORKER_STACK_SIZE_BYTES)
231        .spawn(move || {
232            let runtime = build_runtime()?;
233            runtime.block_on(run_main_with_arg0_guard(
234                path_entry_guard,
235                current_exe,
236                main_fn,
237            ))
238        })?;
239    match handle.join() {
240        Ok(result) => result,
241        Err(payload) => std::panic::resume_unwind(payload),
242    }
243}
244
245async fn run_main_with_arg0_guard<F, Fut>(
246    path_entry_guard: Option<Arg0PathEntryGuard>,
247    current_exe: Option<PathBuf>,
248    main_fn: F,
249) -> anyhow::Result<()>
250where
251    F: FnOnce(Arg0DispatchPaths) -> Fut,
252    Fut: Future<Output = anyhow::Result<()>>,
253{
254    let paths = Arg0DispatchPaths {
255        codex_self_exe: current_exe.clone(),
256        codex_linux_sandbox_exe: if cfg!(target_os = "linux") {
257            linux_sandbox_exe_path(path_entry_guard.as_ref(), current_exe)
258        } else {
259            None
260        },
261        main_execve_wrapper_exe: path_entry_guard
262            .as_ref()
263            .and_then(|path_entry| path_entry.paths().main_execve_wrapper_exe.clone()),
264    };
265
266    let result = main_fn(paths).await;
267    // Keep the arg0 tempdir guard alive until the async entry point finishes;
268    // runtime paths above can point at aliases inside that directory.
269    drop(path_entry_guard);
270    result
271}
272
273fn linux_sandbox_exe_path(
274    path_entry_guard: Option<&Arg0PathEntryGuard>,
275    current_exe: Option<PathBuf>,
276) -> Option<PathBuf> {
277    // Prefer the `codex-linux-sandbox` alias when available so callers can
278    // re-exec through a path whose basename still triggers arg0 dispatch on
279    // bubblewrap builds that do not support `--argv0`.
280    path_entry_guard
281        .and_then(|path_entry| path_entry.paths().codex_linux_sandbox_exe.clone())
282        .or(current_exe)
283}
284
285fn build_runtime() -> anyhow::Result<tokio::runtime::Runtime> {
286    let mut builder = tokio::runtime::Builder::new_multi_thread();
287    builder.enable_all();
288    builder.thread_stack_size(TOKIO_WORKER_STACK_SIZE_BYTES);
289    Ok(builder.build()?)
290}
291
292const ILLEGAL_ENV_VAR_PREFIX: &str = "CODEX_";
293
294/// Load env vars from ~/.codex/.env.
295///
296/// Security: Do not allow `.env` files to create or modify any variables
297/// with names starting with `CODEX_`.
298fn load_dotenv() {
299    if let Ok(codex_home) = find_codex_home()
300        && let Ok(iter) = dotenvy::from_path_iter(codex_home.join(".env"))
301    {
302        set_filtered(iter);
303    }
304}
305
306/// Helper to set vars from a dotenvy iterator while filtering out `CODEX_` keys.
307fn set_filtered<I>(iter: I)
308where
309    I: IntoIterator<Item = Result<(String, String), dotenvy::Error>>,
310{
311    for (key, value) in iter.into_iter().flatten() {
312        if !key.to_ascii_uppercase().starts_with(ILLEGAL_ENV_VAR_PREFIX) {
313            // It is safe to call set_var() because our process is
314            // single-threaded at this point in its execution.
315            unsafe { std::env::set_var(&key, &value) };
316        }
317    }
318}
319
320/// Creates a temporary directory with either:
321///
322/// - UNIX: `apply_patch` symlink to the current executable
323/// - WINDOWS: `apply_patch.bat` batch script to invoke the current executable
324///   with the hidden `--codex-run-as-apply-patch` flag.
325///
326/// Returns the temporary directory guard and the PATH value that prepends the
327/// temporary directory so `apply_patch` can be on the PATH without requiring the
328/// user to install a separate executable, simplifying the deployment of Codex
329/// CLI.
330/// Note: In debug builds the temp-dir guard is disabled to ease local testing.
331///
332/// IMPORTANT: Callers must update PATH before multiple threads are spawned.
333fn prepare_path_entry_for_codex_aliases(
334    existing_path: Option<OsString>,
335) -> std::io::Result<(Arg0PathEntryGuard, OsString)> {
336    let codex_home = find_codex_home()?;
337    #[cfg(not(debug_assertions))]
338    {
339        // Guard against placing helpers in system temp directories outside debug builds.
340        let temp_root = std::env::temp_dir();
341        if codex_home.starts_with(&temp_root) {
342            return Err(std::io::Error::new(
343                std::io::ErrorKind::InvalidInput,
344                format!(
345                    "Refusing to create helper binaries under temporary dir {temp_root:?} (codex_home: {codex_home:?})"
346                ),
347            ));
348        }
349    }
350
351    std::fs::create_dir_all(&codex_home)?;
352    // Use a CODEX_HOME-scoped temp root to avoid cluttering the top-level directory.
353    let temp_root = codex_home.join("tmp").join("arg0");
354    std::fs::create_dir_all(&temp_root)?;
355    #[cfg(unix)]
356    {
357        use std::os::unix::fs::PermissionsExt;
358
359        // Ensure only the current user can access the temp directory.
360        std::fs::set_permissions(&temp_root, std::fs::Permissions::from_mode(0o700))?;
361    }
362
363    // Best-effort cleanup of stale per-session dirs. Ignore failures so startup proceeds.
364    if let Err(err) = janitor_cleanup(&temp_root) {
365        eprintln!("WARNING: failed to clean up stale arg0 temp dirs: {err}");
366    }
367
368    let temp_dir = tempfile::Builder::new()
369        .prefix("codex-arg0")
370        .tempdir_in(&temp_root)?;
371    let path = temp_dir.path();
372
373    let lock_path = path.join(LOCK_FILENAME);
374    let lock_file = File::options()
375        .read(true)
376        .write(true)
377        .create(true)
378        .truncate(false)
379        .open(&lock_path)?;
380    lock_file.try_lock()?;
381
382    for filename in &[
383        APPLY_PATCH_ARG0,
384        MISSPELLED_APPLY_PATCH_ARG0,
385        #[cfg(target_os = "linux")]
386        CODEX_LINUX_SANDBOX_ARG0,
387        #[cfg(unix)]
388        EXECVE_WRAPPER_ARG0,
389    ] {
390        let exe = std::env::current_exe()?;
391
392        #[cfg(unix)]
393        {
394            let link = path.join(filename);
395            symlink(&exe, &link)?;
396        }
397
398        #[cfg(windows)]
399        {
400            let batch_script = path.join(format!("{filename}.bat"));
401            let exe = exe.display();
402            std::fs::write(
403                &batch_script,
404                format!(
405                    r#"@echo off
406"{exe}" {CODEX_CORE_APPLY_PATCH_ARG1} %*
407"#,
408                ),
409            )?;
410        }
411    }
412
413    let updated_path_env_var = path_env_with_entry(path, existing_path);
414
415    let paths = Arg0DispatchPaths {
416        codex_self_exe: std::env::current_exe().ok(),
417        codex_linux_sandbox_exe: {
418            #[cfg(target_os = "linux")]
419            {
420                Some(path.join(CODEX_LINUX_SANDBOX_ARG0))
421            }
422            #[cfg(not(target_os = "linux"))]
423            {
424                None
425            }
426        },
427        main_execve_wrapper_exe: {
428            #[cfg(unix)]
429            {
430                Some(path.join(EXECVE_WRAPPER_ARG0))
431            }
432            #[cfg(not(unix))]
433            {
434                None
435            }
436        },
437    };
438
439    Ok((
440        Arg0PathEntryGuard::new(temp_dir, lock_file, paths),
441        updated_path_env_var,
442    ))
443}
444
445fn path_env_with_package_path_dir(
446    install_context: &InstallContext,
447    existing_path: Option<OsString>,
448) -> Option<OsString> {
449    let path_dir = install_context
450        .package_layout
451        .as_ref()
452        .and_then(|package_layout| package_layout.path_dir.as_ref())?;
453    Some(path_env_with_entry(path_dir.as_path(), existing_path))
454}
455
456fn path_env_with_entry(path_entry: &Path, existing_path: Option<OsString>) -> OsString {
457    #[cfg(unix)]
458    const PATH_SEPARATOR: &str = ":";
459
460    #[cfg(windows)]
461    const PATH_SEPARATOR: &str = ";";
462
463    let capacity = path_entry.as_os_str().len()
464        + existing_path
465            .as_ref()
466            .map_or(0, |existing_path| 1 + existing_path.len());
467    let mut path_env_var = OsString::with_capacity(capacity);
468    path_env_var.push(path_entry);
469    if let Some(existing_path) = existing_path {
470        path_env_var.push(PATH_SEPARATOR);
471        path_env_var.push(existing_path);
472    }
473    path_env_var
474}
475
476fn janitor_cleanup(temp_root: &Path) -> std::io::Result<()> {
477    let entries = match std::fs::read_dir(temp_root) {
478        Ok(entries) => entries,
479        Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(()),
480        Err(err) => return Err(err),
481    };
482
483    for entry in entries.flatten() {
484        let path = entry.path();
485        if !path.is_dir() {
486            continue;
487        }
488
489        // Skip the directory if locking fails or the lock is currently held.
490        let Some(_lock_file) = try_lock_dir(&path)? else {
491            continue;
492        };
493
494        match std::fs::remove_dir_all(&path) {
495            Ok(()) => {}
496            // Expected TOCTOU race: directory can disappear after read_dir/lock checks.
497            Err(err) if err.kind() == std::io::ErrorKind::NotFound => continue,
498            Err(err) => return Err(err),
499        }
500    }
501
502    Ok(())
503}
504
505fn try_lock_dir(dir: &Path) -> std::io::Result<Option<File>> {
506    let lock_path = dir.join(LOCK_FILENAME);
507    let lock_file = match File::options().read(true).write(true).open(&lock_path) {
508        Ok(file) => file,
509        Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
510        Err(err) => return Err(err),
511    };
512
513    match lock_file.try_lock() {
514        Ok(()) => Ok(Some(lock_file)),
515        Err(std::fs::TryLockError::WouldBlock) => Ok(None),
516        Err(err) => Err(err.into()),
517    }
518}
519
520#[cfg(test)]
521mod tests {
522    use super::Arg0DispatchPaths;
523    use super::Arg0PathEntryGuard;
524    use super::LOCK_FILENAME;
525    use super::janitor_cleanup;
526    use super::linux_sandbox_exe_path;
527    #[cfg(unix)]
528    use super::run_main_with_arg0_guard;
529    #[cfg(unix)]
530    use anyhow::ensure;
531    use codex_install_context::CodexPackageLayout;
532    use codex_install_context::InstallContext;
533    use codex_install_context::InstallMethod;
534    use codex_utils_absolute_path::AbsolutePathBuf;
535    use pretty_assertions::assert_eq;
536    use std::fs;
537    use std::fs::File;
538    use std::path::Path;
539    use std::path::PathBuf;
540    use tempfile::TempDir;
541
542    struct PackagePathTestFixture {
543        _temp_dir: TempDir,
544        arg0_dir: PathBuf,
545        existing_dir: PathBuf,
546        install_context: InstallContext,
547        path_dir: AbsolutePathBuf,
548    }
549
550    fn create_lock(dir: &Path) -> std::io::Result<File> {
551        let lock_path = dir.join(LOCK_FILENAME);
552        File::options()
553            .read(true)
554            .write(true)
555            .create(true)
556            .truncate(false)
557            .open(lock_path)
558    }
559
560    fn package_path_test_fixture() -> anyhow::Result<PackagePathTestFixture> {
561        let temp_dir = TempDir::new()?;
562        let arg0_dir = temp_dir.path().join("arg0");
563        let package_dir = temp_dir.path().join("package");
564        let bin_dir = package_dir.join("bin");
565        let path_dir = package_dir.join("codex-path");
566        let existing_dir = temp_dir.path().join("existing-bin");
567        fs::create_dir_all(&arg0_dir)?;
568        fs::create_dir_all(&bin_dir)?;
569        fs::create_dir_all(&path_dir)?;
570        fs::create_dir_all(&existing_dir)?;
571        let path_dir = AbsolutePathBuf::from_absolute_path(path_dir.canonicalize()?)?;
572        let install_context = InstallContext {
573            method: InstallMethod::Other,
574            package_layout: Some(CodexPackageLayout {
575                package_dir: AbsolutePathBuf::from_absolute_path(package_dir.canonicalize()?)?,
576                bin_dir: AbsolutePathBuf::from_absolute_path(bin_dir.canonicalize()?)?,
577                resources_dir: None,
578                path_dir: Some(path_dir.clone()),
579            }),
580        };
581
582        Ok(PackagePathTestFixture {
583            _temp_dir: temp_dir,
584            arg0_dir,
585            existing_dir,
586            install_context,
587            path_dir,
588        })
589    }
590
591    #[test]
592    fn linux_sandbox_exe_path_prefers_codex_linux_sandbox_alias() -> std::io::Result<()> {
593        let temp_dir = TempDir::new()?;
594        let lock_file = create_lock(temp_dir.path())?;
595        let alias_path = temp_dir.path().join("codex-linux-sandbox");
596        let path_entry = Arg0PathEntryGuard::new(
597            temp_dir,
598            lock_file,
599            Arg0DispatchPaths {
600                codex_self_exe: Some(PathBuf::from("/usr/bin/codex")),
601                codex_linux_sandbox_exe: Some(alias_path.clone()),
602                main_execve_wrapper_exe: None,
603            },
604        );
605
606        assert_eq!(
607            linux_sandbox_exe_path(Some(&path_entry), Some(PathBuf::from("/usr/bin/codex"))),
608            Some(alias_path),
609        );
610        Ok(())
611    }
612
613    #[test]
614    fn path_env_can_prepend_package_path_before_arg0_alias_dir() -> anyhow::Result<()> {
615        let fixture = package_path_test_fixture()?;
616
617        let package_path = super::path_env_with_package_path_dir(
618            &fixture.install_context,
619            Some(fixture.existing_dir.as_os_str().to_owned()),
620        )
621        .expect("package path dir should update PATH");
622        let updated_path = super::path_env_with_entry(&fixture.arg0_dir, Some(package_path));
623
624        assert_eq!(
625            std::env::split_paths(&updated_path).collect::<Vec<_>>(),
626            vec![
627                fixture.arg0_dir,
628                fixture.path_dir.as_path().to_path_buf(),
629                fixture.existing_dir
630            ],
631        );
632        Ok(())
633    }
634
635    #[test]
636    fn package_path_survives_arg0_alias_setup_failure() -> anyhow::Result<()> {
637        let fixture = package_path_test_fixture()?;
638
639        let (path_entry_guard, updated_path_env_var) = super::prepare_path_env_var_with_aliases(
640            &fixture.install_context,
641            Some(fixture.existing_dir.as_os_str().to_owned()),
642            |path_for_aliases| {
643                assert_eq!(
644                    std::env::split_paths(
645                        &path_for_aliases.expect("package PATH should be passed to alias setup")
646                    )
647                    .collect::<Vec<_>>(),
648                    vec![
649                        fixture.path_dir.as_path().to_path_buf(),
650                        fixture.existing_dir.clone()
651                    ],
652                );
653                Err(std::io::Error::other("alias setup failed"))
654            },
655        );
656
657        assert!(path_entry_guard.is_none());
658        let updated_path_env_var =
659            updated_path_env_var.expect("package PATH should survive alias setup failure");
660        assert_eq!(
661            std::env::split_paths(&updated_path_env_var).collect::<Vec<_>>(),
662            vec![
663                fixture.path_dir.as_path().to_path_buf(),
664                fixture.existing_dir
665            ],
666        );
667        Ok(())
668    }
669
670    #[cfg(unix)]
671    #[test]
672    fn run_main_with_arg0_guard_keeps_aliases_alive_until_main_returns() -> anyhow::Result<()> {
673        let temp_dir = TempDir::new()?;
674        let alias_path = temp_dir.path().join("codex-helper-alias");
675        fs::write(&alias_path, b"")?;
676        let lock_file = create_lock(temp_dir.path())?;
677        let path_entry = Arg0PathEntryGuard::new(
678            temp_dir,
679            lock_file,
680            Arg0DispatchPaths {
681                codex_self_exe: Some(PathBuf::from("/usr/bin/codex")),
682                codex_linux_sandbox_exe: Some(alias_path.clone()),
683                main_execve_wrapper_exe: Some(alias_path),
684            },
685        );
686
687        super::build_runtime()?.block_on(run_main_with_arg0_guard(
688            /*path_entry_guard*/ Some(path_entry),
689            Some(PathBuf::from("/usr/bin/codex")),
690            |paths| async move {
691                let alias_path = paths
692                    .codex_linux_sandbox_exe
693                    .or(paths.main_execve_wrapper_exe)
694                    .expect("unix dispatch should create at least one alias path");
695                ensure!(
696                    alias_path.exists(),
697                    "alias path disappeared before main future was polled: {}",
698                    alias_path.display()
699                );
700
701                tokio::task::yield_now().await;
702
703                ensure!(
704                    alias_path.exists(),
705                    "alias path disappeared while main future was running: {}",
706                    alias_path.display()
707                );
708                Ok(())
709            },
710        ))
711    }
712
713    #[test]
714    fn janitor_skips_dirs_without_lock_file() -> std::io::Result<()> {
715        let root = tempfile::tempdir()?;
716        let dir = root.path().join("no-lock");
717        fs::create_dir(&dir)?;
718
719        janitor_cleanup(root.path())?;
720
721        assert!(dir.exists());
722        Ok(())
723    }
724
725    #[test]
726    fn janitor_skips_dirs_with_held_lock() -> std::io::Result<()> {
727        let root = tempfile::tempdir()?;
728        let dir = root.path().join("locked");
729        fs::create_dir(&dir)?;
730        let lock_file = create_lock(&dir)?;
731        lock_file.try_lock()?;
732
733        janitor_cleanup(root.path())?;
734
735        assert!(dir.exists());
736        Ok(())
737    }
738
739    #[test]
740    fn janitor_removes_dirs_with_unlocked_lock() -> std::io::Result<()> {
741        let root = tempfile::tempdir()?;
742        let dir = root.path().join("stale");
743        fs::create_dir(&dir)?;
744        create_lock(&dir)?;
745
746        janitor_cleanup(root.path())?;
747
748        assert!(!dir.exists());
749        Ok(())
750    }
751}