refactor: make unified-exec zsh-fork state explicit (#14633)

## Why

The unified-exec path was carrying zsh-fork state in a partially
flattened way.

First, the decision about whether zsh-fork was active came from feature
selection in `ToolsConfig`, while the real prerequisites lived in
session state. That left the handler and runtime defending against
partially configured cases later.

Second, once zsh-fork was active, its two runtime-only paths were
threaded through the runtime as separate arguments even though they form
one coherent piece of configuration.

This change keeps unified-exec on a single session-derived source of
truth and bundles the zsh-fork-specific paths into a named config type
so the runtime can pass them around as one unit.

In particular, this PR introduces this enum so the `ZshFork` variant can
carry the appropriate state with it:

```rust
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum UnifiedExecShellMode {
    Direct,
    ZshFork(ZshForkConfig),
}

#[derive(Debug, Clone, Eq, PartialEq)]
pub struct ZshForkConfig {
    pub(crate) shell_zsh_path: AbsolutePathBuf,
    pub(crate) main_execve_wrapper_exe: AbsolutePathBuf,
}
```

This cleanup was done in preparation for
https://github.com/openai/codex/pull/13432.

## What Changed

- Replaced the feature-only `UnifiedExecBackendConfig` split with
`UnifiedExecShellMode` in `codex-rs/core/src/tools/spec.rs`.
- Derived the unified-exec mode from session-backed inputs when building
turn `ToolsConfig`, and preserved that mode across model switches and
review turns.
- Introduced `ZshForkConfig`, which stores the resolved zsh-fork
`AbsolutePathBuf` values for the configured `zsh` binary and `execve`
wrapper.
- Threaded `ZshForkConfig` through unified-exec command construction and
the zsh-fork preparation path so zsh-fork-specific runtime code consumes
a single config object instead of separate path arguments.
- Added focused tests for constructing zsh-fork mode only when session
prerequisites are available, and updated the zsh-fork expectations to be
target-platform aware.

## Testing

- `cargo test -p codex-core zsh_fork --lib`


---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/14633).
* #13432
* __->__ #14633
This commit is contained in:
Michael Bolin
2026-03-13 17:20:01 -07:00
committed by GitHub
Unverified
parent 7fa5201365
commit b859a98e0f
11 changed files with 259 additions and 69 deletions
+17
View File
@@ -873,6 +873,7 @@ impl TurnContext {
sandbox_policy: self.sandbox_policy.get(),
windows_sandbox_level: self.windows_sandbox_level,
})
.with_unified_exec_shell_mode(self.tools_config.unified_exec_shell_mode.clone())
.with_web_search_config(self.tools_config.web_search_config.clone())
.with_allow_login_shell(self.tools_config.allow_login_shell)
.with_agent_roles(config.agent_roles.clone());
@@ -1261,6 +1262,9 @@ impl Session {
session_telemetry: &SessionTelemetry,
provider: ModelProviderInfo,
session_configuration: &SessionConfiguration,
user_shell: &shell::Shell,
shell_zsh_path: Option<&PathBuf>,
main_execve_wrapper_exe: Option<&PathBuf>,
per_turn_config: Config,
model_info: ModelInfo,
models_manager: &ModelsManager,
@@ -1292,6 +1296,11 @@ impl Session {
sandbox_policy: session_configuration.sandbox_policy.get(),
windows_sandbox_level: session_configuration.windows_sandbox_level,
})
.with_unified_exec_shell_mode_for_session(
user_shell,
shell_zsh_path,
main_execve_wrapper_exe,
)
.with_web_search_config(per_turn_config.web_search_config.clone())
.with_allow_login_shell(per_turn_config.permissions.allow_login_shell)
.with_agent_roles(per_turn_config.agent_roles.clone());
@@ -2358,6 +2367,9 @@ impl Session {
&self.services.session_telemetry,
session_configuration.provider.clone(),
&session_configuration,
self.services.user_shell.as_ref(),
self.services.shell_zsh_path.as_ref(),
self.services.main_execve_wrapper_exe.as_ref(),
per_turn_config,
model_info,
&self.services.models_manager,
@@ -5269,6 +5281,11 @@ async fn spawn_review_thread(
sandbox_policy: parent_turn_context.sandbox_policy.get(),
windows_sandbox_level: parent_turn_context.windows_sandbox_level,
})
.with_unified_exec_shell_mode_for_session(
sess.services.user_shell.as_ref(),
sess.services.shell_zsh_path.as_ref(),
sess.services.main_execve_wrapper_exe.as_ref(),
)
.with_web_search_config(None)
.with_allow_login_shell(config.permissions.allow_login_shell)
.with_agent_roles(config.agent_roles.clone());
+6
View File
@@ -2178,6 +2178,9 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
&session_telemetry,
session_configuration.provider.clone(),
&session_configuration,
services.user_shell.as_ref(),
services.shell_zsh_path.as_ref(),
services.main_execve_wrapper_exe.as_ref(),
per_turn_config,
model_info,
&models_manager,
@@ -2850,6 +2853,9 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx(
&session_telemetry,
session_configuration.provider.clone(),
&session_configuration,
services.user_shell.as_ref(),
services.shell_zsh_path.as_ref(),
services.main_execve_wrapper_exe.as_ref(),
per_turn_config,
model_info,
&models_manager,
+1
View File
@@ -102,6 +102,7 @@ fn shell_command_for_invocation(invocation: &ToolInvocation) -> Option<(Vec<Stri
let command = crate::tools::handlers::unified_exec::get_command(
&params,
invocation.session.user_shell(),
&invocation.turn.tools_config.unified_exec_shell_mode,
invocation.turn.tools_config.allow_login_shell,
)
.ok()?;
@@ -19,6 +19,7 @@ use crate::tools::handlers::parse_arguments_with_base_path;
use crate::tools::handlers::resolve_workdir_base_path;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use crate::tools::spec::UnifiedExecShellMode;
use crate::unified_exec::ExecCommandRequest;
use crate::unified_exec::UnifiedExecContext;
use crate::unified_exec::UnifiedExecProcessManager;
@@ -107,6 +108,7 @@ impl ToolHandler for UnifiedExecHandler {
let command = match get_command(
&params,
invocation.session.user_shell(),
&invocation.turn.tools_config.unified_exec_shell_mode,
invocation.turn.tools_config.allow_login_shell,
) {
Ok(command) => command,
@@ -154,6 +156,7 @@ impl ToolHandler for UnifiedExecHandler {
let command = get_command(
&args,
session.user_shell(),
&turn.tools_config.unified_exec_shell_mode,
turn.tools_config.allow_login_shell,
)
.map_err(FunctionCallError::RespondToModel)?;
@@ -323,15 +326,9 @@ impl ToolHandler for UnifiedExecHandler {
pub(crate) fn get_command(
args: &ExecCommandArgs,
session_shell: Arc<Shell>,
shell_mode: &UnifiedExecShellMode,
allow_login_shell: bool,
) -> Result<Vec<String>, String> {
let model_shell = args.shell.as_ref().map(|shell_str| {
let mut shell = get_shell_by_model_provided_path(&PathBuf::from(shell_str));
shell.shell_snapshot = crate::shell::empty_shell_snapshot_receiver();
shell
});
let shell = model_shell.as_ref().unwrap_or(session_shell.as_ref());
let use_login_shell = match args.login {
Some(true) if !allow_login_shell => {
return Err(
@@ -342,7 +339,22 @@ pub(crate) fn get_command(
None => allow_login_shell,
};
Ok(shell.derive_exec_args(&args.cmd, use_login_shell))
match shell_mode {
UnifiedExecShellMode::Direct => {
let model_shell = args.shell.as_ref().map(|shell_str| {
let mut shell = get_shell_by_model_provided_path(&PathBuf::from(shell_str));
shell.shell_snapshot = crate::shell::empty_shell_snapshot_receiver();
shell
});
let shell = model_shell.as_ref().unwrap_or(session_shell.as_ref());
Ok(shell.derive_exec_args(&args.cmd, use_login_shell))
}
UnifiedExecShellMode::ZshFork(zsh_fork_config) => Ok(vec![
zsh_fork_config.shell_zsh_path.to_string_lossy().to_string(),
if use_login_shell { "-lc" } else { "-c" }.to_string(),
args.cmd.clone(),
]),
}
}
#[cfg(test)]
@@ -2,6 +2,7 @@ use super::*;
use crate::shell::default_user_shell;
use crate::tools::handlers::parse_arguments_with_base_path;
use crate::tools::handlers::resolve_workdir_base_path;
use crate::tools::spec::ZshForkConfig;
use codex_protocol::models::FileSystemPermissions;
use codex_protocol::models::PermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
@@ -18,8 +19,13 @@ fn test_get_command_uses_default_shell_when_unspecified() -> anyhow::Result<()>
assert!(args.shell.is_none());
let command =
get_command(&args, Arc::new(default_user_shell()), true).map_err(anyhow::Error::msg)?;
let command = get_command(
&args,
Arc::new(default_user_shell()),
&UnifiedExecShellMode::Direct,
true,
)
.map_err(anyhow::Error::msg)?;
assert_eq!(command.len(), 3);
assert_eq!(command[2], "echo hello");
@@ -34,8 +40,13 @@ fn test_get_command_respects_explicit_bash_shell() -> anyhow::Result<()> {
assert_eq!(args.shell.as_deref(), Some("/bin/bash"));
let command =
get_command(&args, Arc::new(default_user_shell()), true).map_err(anyhow::Error::msg)?;
let command = get_command(
&args,
Arc::new(default_user_shell()),
&UnifiedExecShellMode::Direct,
true,
)
.map_err(anyhow::Error::msg)?;
assert_eq!(command.last(), Some(&"echo hello".to_string()));
if command
@@ -55,8 +66,13 @@ fn test_get_command_respects_explicit_powershell_shell() -> anyhow::Result<()> {
assert_eq!(args.shell.as_deref(), Some("powershell"));
let command =
get_command(&args, Arc::new(default_user_shell()), true).map_err(anyhow::Error::msg)?;
let command = get_command(
&args,
Arc::new(default_user_shell()),
&UnifiedExecShellMode::Direct,
true,
)
.map_err(anyhow::Error::msg)?;
assert_eq!(command[2], "echo hello");
Ok(())
@@ -70,8 +86,13 @@ fn test_get_command_respects_explicit_cmd_shell() -> anyhow::Result<()> {
assert_eq!(args.shell.as_deref(), Some("cmd"));
let command =
get_command(&args, Arc::new(default_user_shell()), true).map_err(anyhow::Error::msg)?;
let command = get_command(
&args,
Arc::new(default_user_shell()),
&UnifiedExecShellMode::Direct,
true,
)
.map_err(anyhow::Error::msg)?;
assert_eq!(command[2], "echo hello");
Ok(())
@@ -82,8 +103,13 @@ fn test_get_command_rejects_explicit_login_when_disallowed() -> anyhow::Result<(
let json = r#"{"cmd": "echo hello", "login": true}"#;
let args: ExecCommandArgs = parse_arguments(json)?;
let err = get_command(&args, Arc::new(default_user_shell()), false)
.expect_err("explicit login should be rejected");
let err = get_command(
&args,
Arc::new(default_user_shell()),
&UnifiedExecShellMode::Direct,
false,
)
.expect_err("explicit login should be rejected");
assert!(
err.contains("login shell is disabled by config"),
@@ -92,6 +118,38 @@ fn test_get_command_rejects_explicit_login_when_disallowed() -> anyhow::Result<(
Ok(())
}
#[test]
fn test_get_command_ignores_explicit_shell_in_zsh_fork_mode() -> anyhow::Result<()> {
let json = r#"{"cmd": "echo hello", "shell": "/bin/bash"}"#;
let args: ExecCommandArgs = parse_arguments(json)?;
let shell_zsh_path = AbsolutePathBuf::from_absolute_path(if cfg!(windows) {
r"C:\opt\codex\zsh"
} else {
"/opt/codex/zsh"
})?;
let shell_mode = UnifiedExecShellMode::ZshFork(ZshForkConfig {
shell_zsh_path: shell_zsh_path.clone(),
main_execve_wrapper_exe: AbsolutePathBuf::from_absolute_path(if cfg!(windows) {
r"C:\opt\codex\codex-execve-wrapper"
} else {
"/opt/codex/codex-execve-wrapper"
})?,
});
let command = get_command(&args, Arc::new(default_user_shell()), &shell_mode, true)
.map_err(anyhow::Error::msg)?;
assert_eq!(
command,
vec![
shell_zsh_path.to_string_lossy().to_string(),
"-lc".to_string(),
"echo hello".to_string()
]
);
Ok(())
}
#[test]
fn exec_command_args_resolve_relative_additional_permissions_against_workdir() -> anyhow::Result<()>
{
@@ -226,20 +226,9 @@ pub(crate) async fn prepare_unified_exec_zsh_fork(
_attempt: &SandboxAttempt<'_>,
ctx: &ToolCtx,
exec_request: ExecRequest,
shell_zsh_path: &std::path::Path,
main_execve_wrapper_exe: &std::path::Path,
) -> Result<Option<PreparedUnifiedExecZshFork>, ToolError> {
let Some(shell_zsh_path) = ctx.session.services.shell_zsh_path.as_ref() else {
tracing::warn!("ZshFork backend specified, but shell_zsh_path is not configured.");
return Ok(None);
};
if !ctx.session.features().enabled(Feature::ShellZshFork) {
tracing::warn!("ZshFork backend specified, but ShellZshFork feature is not enabled.");
return Ok(None);
}
if !matches!(ctx.session.user_shell().shell_type, ShellType::Zsh) {
tracing::warn!("ZshFork backend specified, but user shell is not Zsh.");
return Ok(None);
}
let parsed = match extract_shell_script(&exec_request.command) {
Ok(parsed) => parsed,
Err(err) => {
@@ -282,16 +271,6 @@ pub(crate) async fn prepare_unified_exec_zsh_fork(
codex_linux_sandbox_exe: ctx.turn.codex_linux_sandbox_exe.clone(),
use_legacy_landlock: ctx.turn.features.use_legacy_landlock(),
};
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 escalation_policy = CoreShellActionProvider {
policy: Arc::clone(&exec_policy),
session: Arc::clone(&ctx.session),
@@ -312,8 +291,8 @@ pub(crate) async fn prepare_unified_exec_zsh_fork(
};
let escalate_server = EscalateServer::new(
shell_zsh_path.clone(),
main_execve_wrapper_exe,
shell_zsh_path.to_path_buf(),
main_execve_wrapper_exe.to_path_buf(),
escalation_policy,
);
let escalation_session = escalate_server
@@ -5,6 +5,7 @@ use crate::tools::runtimes::unified_exec::UnifiedExecRequest;
use crate::tools::sandboxing::SandboxAttempt;
use crate::tools::sandboxing::ToolCtx;
use crate::tools::sandboxing::ToolError;
use crate::tools::spec::ZshForkConfig;
use crate::unified_exec::SpawnLifecycleHandle;
pub(crate) struct PreparedUnifiedExecSpawn {
@@ -37,8 +38,9 @@ pub(crate) async fn maybe_prepare_unified_exec(
attempt: &SandboxAttempt<'_>,
ctx: &ToolCtx,
exec_request: ExecRequest,
zsh_fork_config: &ZshForkConfig,
) -> Result<Option<PreparedUnifiedExecSpawn>, ToolError> {
imp::maybe_prepare_unified_exec(req, attempt, ctx, exec_request).await
imp::maybe_prepare_unified_exec(req, attempt, ctx, exec_request, zsh_fork_config).await
}
#[cfg(unix)]
@@ -83,9 +85,17 @@ mod imp {
attempt: &SandboxAttempt<'_>,
ctx: &ToolCtx,
exec_request: ExecRequest,
zsh_fork_config: &ZshForkConfig,
) -> Result<Option<PreparedUnifiedExecSpawn>, ToolError> {
let Some(prepared) =
unix_escalation::prepare_unified_exec_zsh_fork(req, attempt, ctx, exec_request).await?
let Some(prepared) = unix_escalation::prepare_unified_exec_zsh_fork(
req,
attempt,
ctx,
exec_request,
zsh_fork_config.shell_zsh_path.as_path(),
zsh_fork_config.main_execve_wrapper_exe.as_path(),
)
.await?
else {
return Ok(None);
};
@@ -118,8 +128,9 @@ mod imp {
attempt: &SandboxAttempt<'_>,
ctx: &ToolCtx,
exec_request: ExecRequest,
zsh_fork_config: &ZshForkConfig,
) -> Result<Option<PreparedUnifiedExecSpawn>, ToolError> {
let _ = (req, attempt, ctx, exec_request);
let _ = (req, attempt, ctx, exec_request, zsh_fork_config);
Ok(None)
}
}
@@ -32,7 +32,7 @@ use crate::tools::sandboxing::ToolError;
use crate::tools::sandboxing::ToolRuntime;
use crate::tools::sandboxing::sandbox_override_for_first_attempt;
use crate::tools::sandboxing::with_cached_approval;
use crate::tools::spec::UnifiedExecBackendConfig;
use crate::tools::spec::UnifiedExecShellMode;
use crate::unified_exec::NoopSpawnLifecycle;
use crate::unified_exec::UnifiedExecError;
use crate::unified_exec::UnifiedExecProcess;
@@ -71,12 +71,15 @@ pub struct UnifiedExecApprovalKey {
pub struct UnifiedExecRuntime<'a> {
manager: &'a UnifiedExecProcessManager,
backend: UnifiedExecBackendConfig,
shell_mode: UnifiedExecShellMode,
}
impl<'a> UnifiedExecRuntime<'a> {
pub fn new(manager: &'a UnifiedExecProcessManager, backend: UnifiedExecBackendConfig) -> Self {
Self { manager, backend }
pub fn new(manager: &'a UnifiedExecProcessManager, shell_mode: UnifiedExecShellMode) -> Self {
Self {
manager,
shell_mode,
}
}
}
@@ -209,7 +212,7 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
if let Some(network) = req.network.as_ref() {
network.apply_to_env(&mut env);
}
if self.backend == UnifiedExecBackendConfig::ZshFork {
if let UnifiedExecShellMode::ZshFork(zsh_fork_config) = &self.shell_mode {
let spec = build_command_spec(
&command,
&req.cwd,
@@ -223,7 +226,15 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
let exec_env = attempt
.env_for(spec, req.network.as_ref())
.map_err(|err| ToolError::Codex(err.into()))?;
match zsh_fork_backend::maybe_prepare_unified_exec(req, attempt, ctx, exec_env).await? {
match zsh_fork_backend::maybe_prepare_unified_exec(
req,
attempt,
ctx,
exec_env,
zsh_fork_config,
)
.await?
{
Some(prepared) => {
return self
.manager
+68 -12
View File
@@ -8,6 +8,8 @@ use crate::features::Features;
use crate::mcp_connection_manager::ToolInfo;
use crate::models_manager::collaboration_mode_presets::CollaborationModesConfig;
use crate::original_image_detail::can_request_original_image_detail;
use crate::shell::Shell;
use crate::shell::ShellType;
use crate::tools::code_mode::PUBLIC_TOOL_NAME;
use crate::tools::code_mode::WAIT_TOOL_NAME;
use crate::tools::code_mode::is_code_mode_nested_tool;
@@ -46,12 +48,14 @@ use codex_protocol::openai_models::WebSearchToolType;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SubAgentSource;
use codex_utils_absolute_path::AbsolutePathBuf;
use serde::Deserialize;
use serde::Serialize;
use serde_json::Value as JsonValue;
use serde_json::json;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::path::PathBuf;
const TOOL_SEARCH_DESCRIPTION_TEMPLATE: &str =
include_str!("../../templates/search_tool/tool_description.md");
@@ -203,10 +207,46 @@ pub enum ShellCommandBackendConfig {
ZshFork,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum UnifiedExecBackendConfig {
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum UnifiedExecShellMode {
Direct,
ZshFork,
ZshFork(ZshForkConfig),
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct ZshForkConfig {
pub(crate) shell_zsh_path: AbsolutePathBuf,
pub(crate) main_execve_wrapper_exe: AbsolutePathBuf,
}
impl UnifiedExecShellMode {
pub fn for_session(
shell_command_backend: ShellCommandBackendConfig,
user_shell: &Shell,
shell_zsh_path: Option<&PathBuf>,
main_execve_wrapper_exe: Option<&PathBuf>,
) -> Self {
if cfg!(unix)
&& shell_command_backend == ShellCommandBackendConfig::ZshFork
&& matches!(user_shell.shell_type, ShellType::Zsh)
&& let (Some(shell_zsh_path), Some(main_execve_wrapper_exe)) =
(shell_zsh_path, main_execve_wrapper_exe)
&& let (Ok(shell_zsh_path), Ok(main_execve_wrapper_exe)) = (
AbsolutePathBuf::try_from(shell_zsh_path.as_path())
.inspect_err(|e| tracing::warn!("Failed to convert shell_zsh_path `{shell_zsh_path:?}`: {e:?}")),
AbsolutePathBuf::try_from(main_execve_wrapper_exe.as_path()).inspect_err(|e| {
tracing::warn!("Failed to convert main_execve_wrapper_exe `{main_execve_wrapper_exe:?}`: {e:?}")
}),
)
{
Self::ZshFork(ZshForkConfig {
shell_zsh_path,
main_execve_wrapper_exe,
})
} else {
Self::Direct
}
}
}
#[derive(Debug, Clone)]
@@ -214,7 +254,7 @@ pub(crate) struct ToolsConfig {
pub available_models: Vec<ModelPreset>,
pub shell_type: ConfigShellToolType,
shell_command_backend: ShellCommandBackendConfig,
pub unified_exec_backend: UnifiedExecBackendConfig,
pub unified_exec_shell_mode: UnifiedExecShellMode,
pub allow_login_shell: bool,
pub apply_patch_tool_type: Option<ApplyPatchToolType>,
pub web_search_mode: Option<WebSearchMode>,
@@ -300,13 +340,6 @@ impl ToolsConfig {
} else {
ShellCommandBackendConfig::Classic
};
let unified_exec_backend =
if features.enabled(Feature::ShellTool) && features.enabled(Feature::ShellZshFork) {
UnifiedExecBackendConfig::ZshFork
} else {
UnifiedExecBackendConfig::Direct
};
let unified_exec_allowed = unified_exec_allowed_in_environment(
cfg!(target_os = "windows"),
sandbox_policy,
@@ -353,7 +386,7 @@ impl ToolsConfig {
available_models: available_models_ref.to_vec(),
shell_type,
shell_command_backend,
unified_exec_backend,
unified_exec_shell_mode: UnifiedExecShellMode::Direct,
allow_login_shell: true,
apply_patch_tool_type,
web_search_mode: *web_search_mode,
@@ -390,6 +423,29 @@ impl ToolsConfig {
self
}
pub fn with_unified_exec_shell_mode(
mut self,
unified_exec_shell_mode: UnifiedExecShellMode,
) -> Self {
self.unified_exec_shell_mode = unified_exec_shell_mode;
self
}
pub fn with_unified_exec_shell_mode_for_session(
mut self,
user_shell: &Shell,
shell_zsh_path: Option<&PathBuf>,
main_execve_wrapper_exe: Option<&PathBuf>,
) -> Self {
self.unified_exec_shell_mode = UnifiedExecShellMode::for_session(
self.shell_command_backend,
user_shell,
shell_zsh_path,
main_execve_wrapper_exe,
);
self
}
pub fn with_web_search_config(mut self, web_search_config: Option<WebSearchConfig>) -> Self {
self.web_search_config = web_search_config;
self
+39 -2
View File
@@ -2,6 +2,8 @@ use crate::client_common::tools::FreeformTool;
use crate::config::test_config;
use crate::models_manager::manager::ModelsManager;
use crate::models_manager::model_info::with_config_overrides;
use crate::shell::Shell;
use crate::shell::ShellType;
use crate::tools::ToolRouter;
use crate::tools::registry::ConfiguredToolSpec;
use crate::tools::router::ToolRouterParams;
@@ -9,7 +11,9 @@ use codex_app_server_protocol::AppInfo;
use codex_protocol::openai_models::InputModality;
use codex_protocol::openai_models::ModelInfo;
use codex_protocol::openai_models::ModelsResponse;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use std::path::PathBuf;
use super::*;
@@ -1431,6 +1435,11 @@ fn shell_zsh_fork_prefers_shell_command_over_unified_exec() {
sandbox_policy: &SandboxPolicy::DangerFullAccess,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
});
let user_shell = Shell {
shell_type: ShellType::Zsh,
shell_path: PathBuf::from("/bin/zsh"),
shell_snapshot: crate::shell::empty_shell_snapshot_receiver(),
};
assert_eq!(tools_config.shell_type, ConfigShellToolType::ShellCommand);
assert_eq!(
@@ -1438,8 +1447,36 @@ fn shell_zsh_fork_prefers_shell_command_over_unified_exec() {
ShellCommandBackendConfig::ZshFork
);
assert_eq!(
tools_config.unified_exec_backend,
UnifiedExecBackendConfig::ZshFork
tools_config.unified_exec_shell_mode,
UnifiedExecShellMode::Direct
);
assert_eq!(
tools_config
.with_unified_exec_shell_mode_for_session(
&user_shell,
Some(&PathBuf::from(if cfg!(windows) {
r"C:\opt\codex\zsh"
} else {
"/opt/codex/zsh"
})),
Some(&PathBuf::from(if cfg!(windows) {
r"C:\opt\codex\codex-execve-wrapper"
} else {
"/opt/codex/codex-execve-wrapper"
})),
)
.unified_exec_shell_mode,
if cfg!(unix) {
UnifiedExecShellMode::ZshFork(ZshForkConfig {
shell_zsh_path: AbsolutePathBuf::from_absolute_path("/opt/codex/zsh").unwrap(),
main_execve_wrapper_exe: AbsolutePathBuf::from_absolute_path(
"/opt/codex/codex-execve-wrapper",
)
.unwrap(),
})
} else {
UnifiedExecShellMode::Direct
}
);
}
@@ -578,8 +578,10 @@ impl UnifiedExecProcessManager {
Some(context.session.conversation_id),
));
let mut orchestrator = ToolOrchestrator::new();
let mut runtime =
UnifiedExecRuntime::new(self, context.turn.tools_config.unified_exec_backend);
let mut runtime = UnifiedExecRuntime::new(
self,
context.turn.tools_config.unified_exec_shell_mode.clone(),
);
let exec_approval_requirement = context
.session
.services