Skip to main content

agcodex_arg0/
lib.rs

1use std::future::Future;
2use std::path::Path;
3use std::path::PathBuf;
4
5use agcodex_core::CODEX_APPLY_PATCH_ARG1;
6
7/// While we want to deploy the Codex CLI as a single executable for simplicity,
8/// we also want to expose some of its functionality as distinct CLIs, so we use
9/// the "arg0 trick" to determine which CLI to dispatch. This effectively allows
10/// us to simulate deploying multiple executables as a single binary on Mac and
11/// Linux (but not Windows).
12///
13/// When the current executable is invoked through the hard-link or alias named
14/// `codex-linux-sandbox` we *directly* execute
15/// [`codex_linux_sandbox::run_main`] (which never returns). Otherwise we:
16///
17/// 1.  Use [`dotenvy::from_path`] and [`dotenvy::dotenv`] to modify the
18///     environment before creating any threads.
19/// 2.  Construct a Tokio multi-thread runtime.
20/// 3.  Derive the path to the current executable (so children can re-invoke the
21///     sandbox) when running on Linux.
22/// 4.  Execute the provided async `main_fn` inside that runtime, forwarding any
23///     error. Note that `main_fn` receives `codex_linux_sandbox_exe:
24///     Option<PathBuf>`, as an argument, which is generally needed as part of
25///     constructing [`codex_core::config::Config`].
26///
27/// This function should be used to wrap any `main()` function in binary crates
28/// in this workspace that depends on these helper CLIs.
29pub fn arg0_dispatch_or_else<F, Fut>(main_fn: F) -> anyhow::Result<()>
30where
31    F: FnOnce(Option<PathBuf>) -> Fut,
32    Fut: Future<Output = anyhow::Result<()>>,
33{
34    // Determine if we were invoked via the special alias.
35    let mut args = std::env::args_os();
36    let argv0 = args.next().unwrap_or_default();
37    let exe_name = Path::new(&argv0)
38        .file_name()
39        .and_then(|s| s.to_str())
40        .unwrap_or("");
41
42    if exe_name == "agcodex-linux-sandbox" {
43        // Safety: [`run_main`] never returns.
44        agcodex_linux_sandbox::run_main();
45    }
46
47    let argv1 = args.next().unwrap_or_default();
48    if argv1 == CODEX_APPLY_PATCH_ARG1 {
49        let patch_arg = args.next().and_then(|s| s.to_str().map(|s| s.to_owned()));
50        let exit_code = match patch_arg {
51            Some(patch_arg) => {
52                let mut stdout = std::io::stdout();
53                let mut stderr = std::io::stderr();
54                match agcodex_apply_patch::apply_patch(&patch_arg, &mut stdout, &mut stderr) {
55                    Ok(()) => 0,
56                    Err(_) => 1,
57                }
58            }
59            None => {
60                eprintln!("Error: {CODEX_APPLY_PATCH_ARG1} requires a UTF-8 PATCH argument.");
61                1
62            }
63        };
64        std::process::exit(exit_code);
65    }
66
67    // This modifies the environment, which is not thread-safe, so do this
68    // before creating any threads/the Tokio runtime.
69    load_dotenv();
70
71    // Regular invocation – create a Tokio runtime and execute the provided
72    // async entry-point.
73    let runtime = tokio::runtime::Runtime::new()?;
74    runtime.block_on(async move {
75        let codex_linux_sandbox_exe: Option<PathBuf> = if cfg!(target_os = "linux") {
76            std::env::current_exe().ok()
77        } else {
78            None
79        };
80
81        main_fn(codex_linux_sandbox_exe).await
82    })
83}
84
85const ILLEGAL_ENV_VAR_PREFIX: &str = "CODEX_";
86
87/// Load env vars from ~/.agcodex/.env and `$(pwd)/.env`.
88///
89/// Security: Do not allow `.env` files to create or modify any variables
90/// with names starting with `CODEX_`.
91fn load_dotenv() {
92    if let Ok(codex_home) = agcodex_core::config::find_agcodex_home()
93        && let Ok(iter) = dotenvy::from_path_iter(codex_home.join(".env"))
94    {
95        set_filtered(iter);
96    }
97
98    if let Ok(iter) = dotenvy::dotenv_iter() {
99        set_filtered(iter);
100    }
101}
102
103/// Helper to set vars from a dotenvy iterator while filtering out `CODEX_` keys.
104fn set_filtered<I>(iter: I)
105where
106    I: IntoIterator<Item = Result<(String, String), dotenvy::Error>>,
107{
108    for (key, value) in iter.into_iter().flatten() {
109        if !key.to_ascii_uppercase().starts_with(ILLEGAL_ENV_VAR_PREFIX) {
110            // It is safe to call set_var() because our process is
111            // single-threaded at this point in its execution.
112            unsafe { std::env::set_var(&key, &value) };
113        }
114    }
115}