feat: pass helper executable paths via Arg0DispatchPaths (#12719)

## Why

`codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs` previously
located `codex-execve-wrapper` by scanning `PATH` and sibling
directories. That lookup is brittle and can select the wrong binary when
the runtime environment differs from startup assumptions.

We already pass `codex-linux-sandbox` from `codex-arg0`;
`codex-execve-wrapper` should use the same startup-driven path plumbing.

## What changed

- Introduced `Arg0DispatchPaths` in `codex-arg0` to carry both helper
executable paths:
  - `codex_linux_sandbox_exe`
  - `main_execve_wrapper_exe`
- Updated `arg0_dispatch_or_else()` to pass `Arg0DispatchPaths` to
top-level binaries and preserve helper paths created in
`prepend_path_entry_for_codex_aliases()`.
- Threaded `Arg0DispatchPaths` through entrypoints in `cli`, `exec`,
`tui`, `app-server`, and `mcp-server`.
- Added `main_execve_wrapper_exe` to core configuration plumbing
(`Config`, `ConfigOverrides`, and `SessionServices`).
- Updated zsh-fork shell escalation to consume the configured
`main_execve_wrapper_exe` and removed path-sniffing fallback logic.
- Updated app-server config reload paths so reloaded configs keep the
same startup-provided helper executable paths.

## References

- [`Arg0DispatchPaths`
definition](e355b43d5c/codex-rs/arg0/src/lib.rs (L20-L24))
- [`arg0_dispatch_or_else()` forwarding both
paths](e355b43d5c/codex-rs/arg0/src/lib.rs (L145-L176))
- [zsh-fork escalation using configured wrapper
path](e355b43d5c/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs (L109-L150))

## Testing

- `cargo check -p codex-arg0 -p codex-core -p codex-exec -p codex-tui -p
codex-mcp-server -p codex-app-server`
- `cargo test -p codex-arg0`
- `cargo test -p codex-core tools::runtimes:🐚:unix_escalation:: --
--nocapture`
This commit is contained in:
Michael Bolin 2026-02-24 17:44:38 -08:00 committed by GitHub
parent 448fb6ac22
commit e88f74d140
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 184 additions and 112 deletions

View file

@ -168,6 +168,7 @@ use codex_app_server_protocol::WindowsSandboxSetupMode;
use codex_app_server_protocol::WindowsSandboxSetupStartParams;
use codex_app_server_protocol::WindowsSandboxSetupStartResponse;
use codex_app_server_protocol::build_turns_from_rollout_items;
use codex_arg0::Arg0DispatchPaths;
use codex_backend_client::Client as BackendClient;
use codex_chatgpt::connectors;
use codex_cloud_requirements::cloud_requirements_loader;
@ -338,7 +339,7 @@ pub(crate) struct CodexMessageProcessor {
auth_manager: Arc<AuthManager>,
thread_manager: Arc<ThreadManager>,
outgoing: Arc<OutgoingMessageSender>,
codex_linux_sandbox_exe: Option<PathBuf>,
arg0_paths: Arg0DispatchPaths,
config: Arc<Config>,
single_client_mode: bool,
cli_overrides: Vec<(String, TomlValue)>,
@ -362,7 +363,7 @@ pub(crate) struct CodexMessageProcessorArgs {
pub(crate) auth_manager: Arc<AuthManager>,
pub(crate) thread_manager: Arc<ThreadManager>,
pub(crate) outgoing: Arc<OutgoingMessageSender>,
pub(crate) codex_linux_sandbox_exe: Option<PathBuf>,
pub(crate) arg0_paths: Arg0DispatchPaths,
pub(crate) config: Arc<Config>,
pub(crate) cli_overrides: Vec<(String, TomlValue)>,
pub(crate) cloud_requirements: Arc<RwLock<CloudRequirementsLoader>>,
@ -399,7 +400,7 @@ impl CodexMessageProcessor {
auth_manager,
thread_manager,
outgoing,
codex_linux_sandbox_exe,
arg0_paths,
config,
cli_overrides,
cloud_requirements,
@ -410,7 +411,7 @@ impl CodexMessageProcessor {
auth_manager,
thread_manager,
outgoing: outgoing.clone(),
codex_linux_sandbox_exe,
arg0_paths,
config,
single_client_mode,
cli_overrides,
@ -426,7 +427,7 @@ impl CodexMessageProcessor {
async fn load_latest_config(&self) -> Result<Config, JSONRPCErrorError> {
let cloud_requirements = self.current_cloud_requirements();
codex_core::config::ConfigBuilder::default()
let mut config = codex_core::config::ConfigBuilder::default()
.cli_overrides(self.cli_overrides.clone())
.cloud_requirements(cloud_requirements)
.build()
@ -435,7 +436,10 @@ impl CodexMessageProcessor {
code: INTERNAL_ERROR_CODE,
message: format!("failed to reload config: {err}"),
data: None,
})
})?;
config.codex_linux_sandbox_exe = self.arg0_paths.codex_linux_sandbox_exe.clone();
config.main_execve_wrapper_exe = self.arg0_paths.main_execve_wrapper_exe.clone();
Ok(config)
}
fn current_cloud_requirements(&self) -> CloudRequirementsLoader {
@ -1792,7 +1796,7 @@ impl CodexMessageProcessor {
None => self.config.permissions.sandbox_policy.get().clone(),
};
let codex_linux_sandbox_exe = self.config.codex_linux_sandbox_exe.clone();
let codex_linux_sandbox_exe = self.arg0_paths.codex_linux_sandbox_exe.clone();
let outgoing = self.outgoing.clone();
let request_for_task = request;
let sandbox_cwd = self.config.cwd.clone();
@ -1857,7 +1861,8 @@ impl CodexMessageProcessor {
approval_policy,
sandbox_mode,
model_provider,
codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.clone(),
codex_linux_sandbox_exe: self.arg0_paths.codex_linux_sandbox_exe.clone(),
main_execve_wrapper_exe: self.arg0_paths.main_execve_wrapper_exe.clone(),
base_instructions,
developer_instructions,
compact_prompt,
@ -2111,7 +2116,8 @@ impl CodexMessageProcessor {
approval_policy: approval_policy
.map(codex_app_server_protocol::AskForApproval::to_core),
sandbox_mode: sandbox.map(SandboxMode::to_core),
codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.clone(),
codex_linux_sandbox_exe: self.arg0_paths.codex_linux_sandbox_exe.clone(),
main_execve_wrapper_exe: self.arg0_paths.main_execve_wrapper_exe.clone(),
base_instructions,
developer_instructions,
personality,
@ -4325,7 +4331,8 @@ impl CodexMessageProcessor {
approval_policy,
sandbox_mode,
model_provider,
codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.clone(),
codex_linux_sandbox_exe: self.arg0_paths.codex_linux_sandbox_exe.clone(),
main_execve_wrapper_exe: self.arg0_paths.main_execve_wrapper_exe.clone(),
base_instructions,
developer_instructions,
compact_prompt,
@ -4336,7 +4343,8 @@ impl CodexMessageProcessor {
}
None => (
ConfigOverrides {
codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.clone(),
codex_linux_sandbox_exe: self.arg0_paths.codex_linux_sandbox_exe.clone(),
main_execve_wrapper_exe: self.arg0_paths.main_execve_wrapper_exe.clone(),
..Default::default()
},
None,
@ -4520,7 +4528,8 @@ impl CodexMessageProcessor {
approval_policy,
sandbox_mode,
model_provider,
codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.clone(),
codex_linux_sandbox_exe: self.arg0_paths.codex_linux_sandbox_exe.clone(),
main_execve_wrapper_exe: self.arg0_paths.main_execve_wrapper_exe.clone(),
base_instructions,
developer_instructions,
compact_prompt,
@ -4532,7 +4541,8 @@ impl CodexMessageProcessor {
}
None => (
ConfigOverrides {
codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.clone(),
codex_linux_sandbox_exe: self.arg0_paths.codex_linux_sandbox_exe.clone(),
main_execve_wrapper_exe: self.arg0_paths.main_execve_wrapper_exe.clone(),
..Default::default()
},
None,

View file

@ -1,5 +1,6 @@
#![deny(clippy::print_stdout, clippy::print_stderr)]
use codex_arg0::Arg0DispatchPaths;
use codex_cloud_requirements::cloud_requirements_loader;
use codex_core::AuthManager;
use codex_core::config::Config;
@ -12,7 +13,6 @@ use std::collections::HashMap;
use std::collections::HashSet;
use std::io::ErrorKind;
use std::io::Result as IoResult;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::RwLock;
use std::sync::atomic::AtomicBool;
@ -291,13 +291,13 @@ fn log_format_from_env() -> LogFormat {
}
pub async fn run_main(
codex_linux_sandbox_exe: Option<PathBuf>,
arg0_paths: Arg0DispatchPaths,
cli_config_overrides: CliConfigOverrides,
loader_overrides: LoaderOverrides,
default_analytics_enabled: bool,
) -> IoResult<()> {
run_main_with_transport(
codex_linux_sandbox_exe,
arg0_paths,
cli_config_overrides,
loader_overrides,
default_analytics_enabled,
@ -307,7 +307,7 @@ pub async fn run_main(
}
pub async fn run_main_with_transport(
codex_linux_sandbox_exe: Option<PathBuf>,
arg0_paths: Arg0DispatchPaths,
cli_config_overrides: CliConfigOverrides,
loader_overrides: LoaderOverrides,
default_analytics_enabled: bool,
@ -548,7 +548,7 @@ pub async fn run_main_with_transport(
let loader_overrides = loader_overrides_for_config_api;
let mut processor = MessageProcessor::new(MessageProcessorArgs {
outgoing: outgoing_message_sender,
codex_linux_sandbox_exe,
arg0_paths,
config: Arc::new(config),
single_client_mode,
cli_overrides,

View file

@ -1,6 +1,7 @@
use clap::Parser;
use codex_app_server::AppServerTransport;
use codex_app_server::run_main_with_transport;
use codex_arg0::Arg0DispatchPaths;
use codex_arg0::arg0_dispatch_or_else;
use codex_core::config_loader::LoaderOverrides;
use codex_utils_cli::CliConfigOverrides;
@ -23,7 +24,7 @@ struct AppServerArgs {
}
fn main() -> anyhow::Result<()> {
arg0_dispatch_or_else(|codex_linux_sandbox_exe| async move {
arg0_dispatch_or_else(|arg0_paths: Arg0DispatchPaths| async move {
let args = AppServerArgs::parse();
let managed_config_path = managed_config_path_from_debug_env();
let loader_overrides = LoaderOverrides {
@ -33,7 +34,7 @@ fn main() -> anyhow::Result<()> {
let transport = args.listen;
run_main_with_transport(
codex_linux_sandbox_exe,
arg0_paths,
CliConfigOverrides::default(),
loader_overrides,
false,

View file

@ -1,5 +1,4 @@
use std::collections::HashSet;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::RwLock;
use std::sync::atomic::AtomicBool;
@ -32,6 +31,7 @@ use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::ServerNotification;
use codex_app_server_protocol::ServerRequestPayload;
use codex_app_server_protocol::experimental_required_message;
use codex_arg0::Arg0DispatchPaths;
use codex_core::AuthManager;
use codex_core::ThreadManager;
use codex_core::auth::ExternalAuthRefreshContext;
@ -139,7 +139,7 @@ pub(crate) struct ConnectionSessionState {
pub(crate) struct MessageProcessorArgs {
pub(crate) outgoing: Arc<OutgoingMessageSender>,
pub(crate) codex_linux_sandbox_exe: Option<PathBuf>,
pub(crate) arg0_paths: Arg0DispatchPaths,
pub(crate) config: Arc<Config>,
pub(crate) single_client_mode: bool,
pub(crate) cli_overrides: Vec<(String, TomlValue)>,
@ -155,7 +155,7 @@ impl MessageProcessor {
pub(crate) fn new(args: MessageProcessorArgs) -> Self {
let MessageProcessorArgs {
outgoing,
codex_linux_sandbox_exe,
arg0_paths,
config,
single_client_mode,
cli_overrides,
@ -184,7 +184,7 @@ impl MessageProcessor {
auth_manager,
thread_manager,
outgoing: outgoing.clone(),
codex_linux_sandbox_exe,
arg0_paths,
config: Arc::clone(&config),
cli_overrides: cli_overrides.clone(),
cloud_requirements: cloud_requirements.clone(),

View file

@ -17,19 +17,31 @@ const EXECVE_WRAPPER_ARG0: &str = "codex-execve-wrapper";
const LOCK_FILENAME: &str = ".lock";
const TOKIO_WORKER_STACK_SIZE_BYTES: usize = 16 * 1024 * 1024;
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Arg0DispatchPaths {
pub codex_linux_sandbox_exe: Option<PathBuf>,
pub main_execve_wrapper_exe: Option<PathBuf>,
}
/// Keeps the per-session PATH entry alive and locked for the process lifetime.
pub struct Arg0PathEntryGuard {
_temp_dir: TempDir,
_lock_file: File,
paths: Arg0DispatchPaths,
}
impl Arg0PathEntryGuard {
fn new(temp_dir: TempDir, lock_file: File) -> Self {
fn new(temp_dir: TempDir, lock_file: File, paths: Arg0DispatchPaths) -> Self {
Self {
_temp_dir: temp_dir,
_lock_file: lock_file,
paths,
}
}
pub fn paths(&self) -> &Arg0DispatchPaths {
&self.paths
}
}
pub fn arg0_dispatch() -> Option<Arg0PathEntryGuard> {
@ -124,33 +136,43 @@ pub fn arg0_dispatch() -> Option<Arg0PathEntryGuard> {
/// 3. Derive the path to the current executable (so children can re-invoke the
/// sandbox) when running on Linux.
/// 4. Execute the provided async `main_fn` inside that runtime, forwarding any
/// error. Note that `main_fn` receives `codex_linux_sandbox_exe:
/// Option<PathBuf>`, as an argument, which is generally needed as part of
/// constructing [`codex_core::config::Config`].
/// error. Note that `main_fn` receives [`Arg0DispatchPaths`], which
/// contains the helper executable paths needed to construct
/// [`codex_core::config::Config`].
///
/// This function should be used to wrap any `main()` function in binary crates
/// in this workspace that depends on these helper CLIs.
pub fn arg0_dispatch_or_else<F, Fut>(main_fn: F) -> anyhow::Result<()>
where
F: FnOnce(Option<PathBuf>) -> Fut,
F: FnOnce(Arg0DispatchPaths) -> Fut,
Fut: Future<Output = anyhow::Result<()>>,
{
// Retain the TempDir so it exists for the lifetime of the invocation of
// this executable. Admittedly, we could invoke `keep()` on it, but it
// would be nice to avoid leaving temporary directories behind, if possible.
let _path_entry = arg0_dispatch();
let path_entry = arg0_dispatch();
// Regular invocation create a Tokio runtime and execute the provided
// async entry-point.
let runtime = build_runtime()?;
runtime.block_on(async move {
let codex_linux_sandbox_exe: Option<PathBuf> = if cfg!(target_os = "linux") {
std::env::current_exe().ok()
} else {
None
let current_exe = std::env::current_exe().ok();
let paths = Arg0DispatchPaths {
codex_linux_sandbox_exe: if cfg!(target_os = "linux") {
current_exe.or_else(|| {
path_entry
.as_ref()
.and_then(|path_entry| path_entry.paths().codex_linux_sandbox_exe.clone())
})
} else {
None
},
main_execve_wrapper_exe: path_entry
.as_ref()
.and_then(|path_entry| path_entry.paths().main_execve_wrapper_exe.clone()),
};
main_fn(codex_linux_sandbox_exe).await
main_fn(paths).await
})
}
@ -301,7 +323,30 @@ pub fn prepend_path_entry_for_codex_aliases() -> std::io::Result<Arg0PathEntryGu
std::env::set_var("PATH", updated_path_env_var);
}
Ok(Arg0PathEntryGuard::new(temp_dir, lock_file))
let paths = Arg0DispatchPaths {
codex_linux_sandbox_exe: {
#[cfg(target_os = "linux")]
{
Some(path.join(LINUX_SANDBOX_ARG0))
}
#[cfg(not(target_os = "linux"))]
{
None
}
},
main_execve_wrapper_exe: {
#[cfg(unix)]
{
Some(path.join(EXECVE_WRAPPER_ARG0))
}
#[cfg(not(unix))]
{
None
}
},
};
Ok(Arg0PathEntryGuard::new(temp_dir, lock_file, paths))
}
fn janitor_cleanup(temp_root: &Path) -> std::io::Result<()> {

View file

@ -3,6 +3,7 @@ use clap::CommandFactory;
use clap::Parser;
use clap_complete::Shell;
use clap_complete::generate;
use codex_arg0::Arg0DispatchPaths;
use codex_arg0::arg0_dispatch_or_else;
use codex_chatgpt::apply_command::ApplyCommand;
use codex_chatgpt::apply_command::run_apply_command;
@ -543,13 +544,13 @@ fn stage_str(stage: codex_core::features::Stage) -> &'static str {
}
fn main() -> anyhow::Result<()> {
arg0_dispatch_or_else(|codex_linux_sandbox_exe| async move {
cli_main(codex_linux_sandbox_exe).await?;
arg0_dispatch_or_else(|arg0_paths: Arg0DispatchPaths| async move {
cli_main(arg0_paths).await?;
Ok(())
})
}
async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()> {
async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
let MultitoolCli {
config_overrides: mut root_config_overrides,
feature_toggles,
@ -567,7 +568,7 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
&mut interactive.config_overrides,
root_config_overrides.clone(),
);
let exit_info = run_interactive_tui(interactive, codex_linux_sandbox_exe).await?;
let exit_info = run_interactive_tui(interactive, arg0_paths.clone()).await?;
handle_app_exit(exit_info)?;
}
Some(Subcommand::Exec(mut exec_cli)) => {
@ -575,7 +576,7 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
&mut exec_cli.config_overrides,
root_config_overrides.clone(),
);
codex_exec::run_main(exec_cli, codex_linux_sandbox_exe).await?;
codex_exec::run_main(exec_cli, arg0_paths.clone()).await?;
}
Some(Subcommand::Review(review_args)) => {
let mut exec_cli = ExecCli::try_parse_from(["codex", "exec"])?;
@ -584,10 +585,10 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
&mut exec_cli.config_overrides,
root_config_overrides.clone(),
);
codex_exec::run_main(exec_cli, codex_linux_sandbox_exe).await?;
codex_exec::run_main(exec_cli, arg0_paths.clone()).await?;
}
Some(Subcommand::McpServer) => {
codex_mcp_server::run_main(codex_linux_sandbox_exe, root_config_overrides).await?;
codex_mcp_server::run_main(arg0_paths.clone(), root_config_overrides).await?;
}
Some(Subcommand::Mcp(mut mcp_cli)) => {
// Propagate any root-level config overrides (e.g. `-c key=value`).
@ -598,7 +599,7 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
None => {
let transport = app_server_cli.listen;
codex_app_server::run_main_with_transport(
codex_linux_sandbox_exe,
arg0_paths.clone(),
root_config_overrides,
codex_core::config_loader::LoaderOverrides::default(),
app_server_cli.analytics_default_enabled,
@ -642,7 +643,7 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
all,
config_overrides,
);
let exit_info = run_interactive_tui(interactive, codex_linux_sandbox_exe).await?;
let exit_info = run_interactive_tui(interactive, arg0_paths.clone()).await?;
handle_app_exit(exit_info)?;
}
Some(Subcommand::Fork(ForkCommand {
@ -659,7 +660,7 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
all,
config_overrides,
);
let exit_info = run_interactive_tui(interactive, codex_linux_sandbox_exe).await?;
let exit_info = run_interactive_tui(interactive, arg0_paths.clone()).await?;
handle_app_exit(exit_info)?;
}
Some(Subcommand::Login(mut login_cli)) => {
@ -708,7 +709,8 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
&mut cloud_cli.config_overrides,
root_config_overrides.clone(),
);
codex_cloud_tasks::run_main(cloud_cli, codex_linux_sandbox_exe).await?;
codex_cloud_tasks::run_main(cloud_cli, arg0_paths.codex_linux_sandbox_exe.clone())
.await?;
}
Some(Subcommand::Sandbox(sandbox_args)) => match sandbox_args.cmd {
SandboxCommand::Macos(mut seatbelt_cli) => {
@ -718,7 +720,7 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
);
codex_cli::debug_sandbox::run_command_under_seatbelt(
seatbelt_cli,
codex_linux_sandbox_exe,
arg0_paths.codex_linux_sandbox_exe.clone(),
)
.await?;
}
@ -729,7 +731,7 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
);
codex_cli::debug_sandbox::run_command_under_landlock(
landlock_cli,
codex_linux_sandbox_exe,
arg0_paths.codex_linux_sandbox_exe.clone(),
)
.await?;
}
@ -740,7 +742,7 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
);
codex_cli::debug_sandbox::run_command_under_windows(
windows_cli,
codex_linux_sandbox_exe,
arg0_paths.codex_linux_sandbox_exe.clone(),
)
.await?;
}
@ -887,7 +889,7 @@ fn prepend_config_flags(
async fn run_interactive_tui(
mut interactive: TuiCli,
codex_linux_sandbox_exe: Option<PathBuf>,
arg0_paths: Arg0DispatchPaths,
) -> std::io::Result<AppExitInfo> {
if let Some(prompt) = interactive.prompt.take() {
// Normalize CRLF/CR to LF so CLI-provided text can't leak `\r` into TUI state.
@ -912,7 +914,7 @@ async fn run_interactive_tui(
}
}
codex_tui::run_main(interactive, codex_linux_sandbox_exe).await
codex_tui::run_main(interactive, arg0_paths).await
}
fn confirm(prompt: &str) -> std::io::Result<bool> {

View file

@ -87,6 +87,7 @@ pub(crate) async fn apply_role_to_config(
ConfigOverrides {
cwd: Some(config.cwd.clone()),
codex_linux_sandbox_exe: config.codex_linux_sandbox_exe.clone(),
main_execve_wrapper_exe: config.main_execve_wrapper_exe.clone(),
js_repl_node_path: config.js_repl_node_path.clone(),
..Default::default()
},
@ -340,6 +341,8 @@ mod tests {
TomlValue::String("base-model".to_string()),
)])
.await;
config.codex_linux_sandbox_exe = Some(PathBuf::from("/tmp/codex-linux-sandbox"));
config.main_execve_wrapper_exe = Some(PathBuf::from("/tmp/codex-execve-wrapper"));
let role_path = write_role_config(
&home,
"effort-only.toml",
@ -360,6 +363,14 @@ mod tests {
assert_eq!(config.model.as_deref(), Some("base-model"));
assert_eq!(config.model_reasoning_effort, Some(ReasoningEffort::High));
assert_eq!(
config.codex_linux_sandbox_exe,
Some(PathBuf::from("/tmp/codex-linux-sandbox"))
);
assert_eq!(
config.main_execve_wrapper_exe,
Some(PathBuf::from("/tmp/codex-execve-wrapper"))
);
}
#[tokio::test]

View file

@ -1331,6 +1331,7 @@ impl Session {
config.background_terminal_max_timeout,
),
shell_zsh_path: config.zsh_path.clone(),
main_execve_wrapper_exe: config.main_execve_wrapper_exe.clone(),
analytics_events_client: AnalyticsEventsClient::new(
Arc::clone(&config),
Arc::clone(&auth_manager),
@ -8201,6 +8202,7 @@ mod tests {
config.background_terminal_max_timeout,
),
shell_zsh_path: None,
main_execve_wrapper_exe: config.main_execve_wrapper_exe.clone(),
analytics_events_client: AnalyticsEventsClient::new(
Arc::clone(&config),
Arc::clone(&auth_manager),
@ -8356,6 +8358,7 @@ mod tests {
config.background_terminal_max_timeout,
),
shell_zsh_path: None,
main_execve_wrapper_exe: config.main_execve_wrapper_exe.clone(),
analytics_events_client: AnalyticsEventsClient::new(
Arc::clone(&config),
Arc::clone(&auth_manager),

View file

@ -399,6 +399,11 @@ pub struct Config {
/// When this program is invoked, arg0 will be set to `codex-linux-sandbox`.
pub codex_linux_sandbox_exe: Option<PathBuf>,
/// Path to the `codex-execve-wrapper` executable used for shell
/// escalation. This cannot be set in the config file: it must be set in
/// code via [`ConfigOverrides`].
pub main_execve_wrapper_exe: Option<PathBuf>,
/// Optional absolute path to the Node runtime used by `js_repl`.
pub js_repl_node_path: Option<PathBuf>,
@ -646,7 +651,8 @@ impl Config {
/// designed to use [AskForApproval::Never] exclusively.
///
/// Further, [ConfigOverrides] contains some options that are not supported
/// in [ConfigToml], such as `cwd` and `codex_linux_sandbox_exe`.
/// in [ConfigToml], such as `cwd`, `codex_linux_sandbox_exe`, and
/// `main_execve_wrapper_exe`.
pub async fn load_with_cli_overrides_and_harness_overrides(
cli_overrides: Vec<(String, TomlValue)>,
harness_overrides: ConfigOverrides,
@ -1536,6 +1542,7 @@ pub struct ConfigOverrides {
pub model_provider: Option<String>,
pub config_profile: Option<String>,
pub codex_linux_sandbox_exe: Option<PathBuf>,
pub main_execve_wrapper_exe: Option<PathBuf>,
pub js_repl_node_path: Option<PathBuf>,
pub js_repl_node_module_dirs: Option<Vec<PathBuf>>,
pub zsh_path: Option<PathBuf>,
@ -1665,6 +1672,7 @@ impl Config {
model_provider,
config_profile: config_profile_key,
codex_linux_sandbox_exe,
main_execve_wrapper_exe,
js_repl_node_path: js_repl_node_path_override,
js_repl_node_module_dirs: js_repl_node_module_dirs_override,
zsh_path: zsh_path_override,
@ -2151,6 +2159,7 @@ impl Config {
ephemeral: ephemeral.unwrap_or_default(),
file_opener: cfg.file_opener.unwrap_or(UriBasedFileOpener::VsCode),
codex_linux_sandbox_exe,
main_execve_wrapper_exe,
js_repl_node_path,
js_repl_node_module_dirs,
zsh_path,
@ -4765,6 +4774,7 @@ model_verbosity = "high"
ephemeral: false,
file_opener: UriBasedFileOpener::VsCode,
codex_linux_sandbox_exe: None,
main_execve_wrapper_exe: None,
js_repl_node_path: None,
js_repl_node_module_dirs: Vec::new(),
zsh_path: None,
@ -4891,6 +4901,7 @@ model_verbosity = "high"
ephemeral: false,
file_opener: UriBasedFileOpener::VsCode,
codex_linux_sandbox_exe: None,
main_execve_wrapper_exe: None,
js_repl_node_path: None,
js_repl_node_module_dirs: Vec::new(),
zsh_path: None,
@ -5015,6 +5026,7 @@ model_verbosity = "high"
ephemeral: false,
file_opener: UriBasedFileOpener::VsCode,
codex_linux_sandbox_exe: None,
main_execve_wrapper_exe: None,
js_repl_node_path: None,
js_repl_node_module_dirs: Vec::new(),
zsh_path: None,
@ -5125,6 +5137,7 @@ model_verbosity = "high"
ephemeral: false,
file_opener: UriBasedFileOpener::VsCode,
codex_linux_sandbox_exe: None,
main_execve_wrapper_exe: None,
js_repl_node_path: None,
js_repl_node_module_dirs: Vec::new(),
zsh_path: None,

View file

@ -29,6 +29,8 @@ pub(crate) struct SessionServices {
pub(crate) unified_exec_manager: UnifiedExecProcessManager,
#[cfg_attr(not(unix), allow(dead_code))]
pub(crate) shell_zsh_path: Option<PathBuf>,
#[cfg_attr(not(unix), allow(dead_code))]
pub(crate) main_execve_wrapper_exe: Option<PathBuf>,
pub(crate) analytics_events_client: AnalyticsEventsClient,
pub(crate) hooks: Hooks,
pub(crate) rollout: Mutex<Option<RolloutRecorder>>,

View file

@ -106,15 +106,22 @@ pub(super) async fn try_run_zsh_fork(
justification,
arg0,
};
let main_execve_wrapper_exe = ctx
.session
.services
.main_execve_wrapper_exe
.clone()
.ok_or_else(|| {
ToolError::Rejected(
"zsh fork feature enabled, but execve wrapper is not configured".to_string(),
)
})?;
let exec_params = ExecParams {
command: script,
workdir: req.cwd.to_string_lossy().to_string(),
timeout_ms: Some(effective_timeout.as_millis() as u64),
login: Some(login),
};
let execve_wrapper =
shell_execve_wrapper().map_err(|err| ToolError::Rejected(format!("{err}")))?;
// Note that Stopwatch starts immediately upon creation, so currently we try
// to minimize the time between creating the Stopwatch and starting the
@ -132,8 +139,11 @@ pub(super) async fn try_run_zsh_fork(
stopwatch: stopwatch.clone(),
};
let escalate_server =
EscalateServer::new(shell_zsh_path.clone(), execve_wrapper, escalation_policy);
let escalate_server = EscalateServer::new(
shell_zsh_path.clone(),
main_execve_wrapper_exe,
escalation_policy,
);
let exec_result = escalate_server
.exec(exec_params, cancel_token, &command_executor)
@ -342,34 +352,6 @@ impl ShellCommandExecutor for CoreShellCommandExecutor {
}
}
// TODO(mbolin): This should be passed down from codex-arg0 like codex_linux_sandbox_exe.
fn shell_execve_wrapper() -> anyhow::Result<PathBuf> {
const EXECVE_WRAPPER: &str = "codex-execve-wrapper";
if let Some(path) = std::env::var_os("PATH") {
for dir in std::env::split_paths(&path) {
let candidate = dir.join(EXECVE_WRAPPER);
if candidate.is_file() {
return Ok(candidate);
}
}
}
let exe = std::env::current_exe()?;
let sibling = exe
.parent()
.map(|parent| parent.join(EXECVE_WRAPPER))
.ok_or_else(|| anyhow::anyhow!("failed to determine codex-execve-wrapper path"))?;
if sibling.is_file() {
return Ok(sibling);
}
Err(anyhow::anyhow!(
"failed to locate {EXECVE_WRAPPER} in PATH or next to current executable ({})",
exe.display()
))
}
#[derive(Debug, Eq, PartialEq)]
struct ParsedShellCommand {
script: String,

View file

@ -13,6 +13,7 @@ pub mod exec_events;
pub use cli::Cli;
pub use cli::Command;
pub use cli::ReviewArgs;
use codex_arg0::Arg0DispatchPaths;
use codex_cloud_requirements::cloud_requirements_loader;
use codex_core::AuthManager;
use codex_core::LMSTUDIO_OSS_PROVIDER_ID;
@ -90,7 +91,7 @@ struct ThreadEventEnvelope {
suppress_output: bool,
}
pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()> {
pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
if let Err(err) = set_default_originator("codex_exec".to_string()) {
tracing::warn!(?err, "Failed to set codex exec originator override {err:?}");
}
@ -271,7 +272,8 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
sandbox_mode,
cwd: resolved_cwd,
model_provider: model_provider.clone(),
codex_linux_sandbox_exe,
codex_linux_sandbox_exe: arg0_paths.codex_linux_sandbox_exe.clone(),
main_execve_wrapper_exe: arg0_paths.main_execve_wrapper_exe.clone(),
js_repl_node_path: None,
js_repl_node_module_dirs: None,
zsh_path: None,

View file

@ -10,6 +10,7 @@
//! This allows us to ship a completely separate set of functionality as part
//! of the `codex-exec` binary.
use clap::Parser;
use codex_arg0::Arg0DispatchPaths;
use codex_arg0::arg0_dispatch_or_else;
use codex_exec::Cli;
use codex_exec::run_main;
@ -25,7 +26,7 @@ struct TopCli {
}
fn main() -> anyhow::Result<()> {
arg0_dispatch_or_else(|codex_linux_sandbox_exe| async move {
arg0_dispatch_or_else(|arg0_paths: Arg0DispatchPaths| async move {
let top_cli = TopCli::parse();
// Merge root-level overrides into inner CLI struct so downstream logic remains unchanged.
let mut inner = top_cli.inner;
@ -34,7 +35,7 @@ fn main() -> anyhow::Result<()> {
.raw_overrides
.splice(0..0, top_cli.config_overrides.raw_overrides);
run_main(inner, codex_linux_sandbox_exe).await?;
run_main(inner, arg0_paths).await?;
Ok(())
})
}

View file

@ -1,5 +1,6 @@
//! Configuration object accepted by the `codex` MCP tool-call.
use codex_arg0::Arg0DispatchPaths;
use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
use codex_protocol::ThreadId;
@ -153,7 +154,7 @@ impl CodexToolCallParam {
/// effective Config object generated from the supplied parameters.
pub async fn into_config(
self,
codex_linux_sandbox_exe: Option<PathBuf>,
arg0_paths: Arg0DispatchPaths,
) -> std::io::Result<(String, Config)> {
let Self {
prompt,
@ -175,7 +176,8 @@ impl CodexToolCallParam {
cwd: cwd.map(PathBuf::from),
approval_policy: approval_policy.map(Into::into),
sandbox_mode: sandbox.map(Into::into),
codex_linux_sandbox_exe,
codex_linux_sandbox_exe: arg0_paths.codex_linux_sandbox_exe.clone(),
main_execve_wrapper_exe: arg0_paths.main_execve_wrapper_exe.clone(),
base_instructions,
developer_instructions,
compact_prompt,

View file

@ -3,8 +3,8 @@
use std::io::ErrorKind;
use std::io::Result as IoResult;
use std::path::PathBuf;
use codex_arg0::Arg0DispatchPaths;
use codex_core::config::Config;
use codex_utils_cli::CliConfigOverrides;
@ -49,7 +49,7 @@ const CHANNEL_CAPACITY: usize = 128;
type IncomingMessage = JsonRpcMessage<ClientRequest, Value, ClientNotification>;
pub async fn run_main(
codex_linux_sandbox_exe: Option<PathBuf>,
arg0_paths: Arg0DispatchPaths,
cli_config_overrides: CliConfigOverrides,
) -> IoResult<()> {
// Install a simple subscriber so `tracing` output is visible. Users can
@ -105,7 +105,7 @@ pub async fn run_main(
let outgoing_message_sender = OutgoingMessageSender::new(outgoing_tx);
let mut processor = MessageProcessor::new(
outgoing_message_sender,
codex_linux_sandbox_exe,
arg0_paths,
std::sync::Arc::new(config),
);
async move {

View file

@ -1,10 +1,11 @@
use codex_arg0::Arg0DispatchPaths;
use codex_arg0::arg0_dispatch_or_else;
use codex_mcp_server::run_main;
use codex_utils_cli::CliConfigOverrides;
fn main() -> anyhow::Result<()> {
arg0_dispatch_or_else(|codex_linux_sandbox_exe| async move {
run_main(codex_linux_sandbox_exe, CliConfigOverrides::default()).await?;
arg0_dispatch_or_else(|arg0_paths: Arg0DispatchPaths| async move {
run_main(arg0_paths, CliConfigOverrides::default()).await?;
Ok(())
})
}

View file

@ -1,6 +1,6 @@
use std::collections::HashMap;
use std::path::PathBuf;
use codex_arg0::Arg0DispatchPaths;
use codex_core::AuthManager;
use codex_core::ThreadManager;
use codex_core::config::Config;
@ -38,7 +38,7 @@ use crate::outgoing_message::OutgoingMessageSender;
pub(crate) struct MessageProcessor {
outgoing: Arc<OutgoingMessageSender>,
initialized: bool,
codex_linux_sandbox_exe: Option<PathBuf>,
arg0_paths: Arg0DispatchPaths,
thread_manager: Arc<ThreadManager>,
running_requests_id_to_codex_uuid: Arc<Mutex<HashMap<RequestId, ThreadId>>>,
}
@ -48,7 +48,7 @@ impl MessageProcessor {
/// `Sender` so handlers can enqueue messages to be written to stdout.
pub(crate) fn new(
outgoing: OutgoingMessageSender,
codex_linux_sandbox_exe: Option<PathBuf>,
arg0_paths: Arg0DispatchPaths,
config: Arc<Config>,
) -> Self {
let outgoing = Arc::new(outgoing);
@ -66,7 +66,7 @@ impl MessageProcessor {
Self {
outgoing,
initialized: false,
codex_linux_sandbox_exe,
arg0_paths,
thread_manager,
running_requests_id_to_codex_uuid: Arc::new(Mutex::new(HashMap::new())),
}
@ -352,10 +352,7 @@ impl MessageProcessor {
let arguments = arguments.map(serde_json::Value::Object);
let (initial_prompt, config): (String, Config) = match arguments {
Some(json_val) => match serde_json::from_value::<CodexToolCallParam>(json_val) {
Ok(tool_cfg) => match tool_cfg
.into_config(self.codex_linux_sandbox_exe.clone())
.await
{
Ok(tool_cfg) => match tool_cfg.into_config(self.arg0_paths.clone()).await {
Ok(cfg) => cfg,
Err(e) => {
let result = CallToolResult {

View file

@ -216,15 +216,13 @@ use crate::onboarding::onboarding_screen::OnboardingScreenArgs;
use crate::onboarding::onboarding_screen::run_onboarding_app;
use crate::tui::Tui;
pub use cli::Cli;
use codex_arg0::Arg0DispatchPaths;
pub use markdown_render::render_markdown_text;
pub use public_widgets::composer_input::ComposerAction;
pub use public_widgets::composer_input::ComposerInput;
// (tests access modules directly within the crate)
pub async fn run_main(
mut cli: Cli,
codex_linux_sandbox_exe: Option<PathBuf>,
) -> std::io::Result<AppExitInfo> {
pub async fn run_main(mut cli: Cli, arg0_paths: Arg0DispatchPaths) -> std::io::Result<AppExitInfo> {
let (sandbox_mode, approval_policy) = if cli.full_auto {
(
Some(SandboxMode::WorkspaceWrite),
@ -373,7 +371,8 @@ pub async fn run_main(
cwd,
model_provider: model_provider_override.clone(),
config_profile: cli.config_profile.clone(),
codex_linux_sandbox_exe,
codex_linux_sandbox_exe: arg0_paths.codex_linux_sandbox_exe.clone(),
main_execve_wrapper_exe: arg0_paths.main_execve_wrapper_exe.clone(),
show_raw_agent_reasoning: cli.oss.then_some(true),
additional_writable_roots: additional_dirs,
..Default::default()

View file

@ -1,4 +1,5 @@
use clap::Parser;
use codex_arg0::Arg0DispatchPaths;
use codex_arg0::arg0_dispatch_or_else;
use codex_tui::Cli;
use codex_tui::run_main;
@ -14,14 +15,14 @@ struct TopCli {
}
fn main() -> anyhow::Result<()> {
arg0_dispatch_or_else(|codex_linux_sandbox_exe| async move {
arg0_dispatch_or_else(|arg0_paths: Arg0DispatchPaths| async move {
let top_cli = TopCli::parse();
let mut inner = top_cli.inner;
inner
.config_overrides
.raw_overrides
.splice(0..0, top_cli.config_overrides.raw_overrides);
let exit_info = run_main(inner, codex_linux_sandbox_exe).await?;
let exit_info = run_main(inner, arg0_paths).await?;
let token_usage = exit_info.token_usage;
if !token_usage.is_zero() {
println!(