mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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:
committed by
GitHub
Unverified
parent
7fa5201365
commit
b859a98e0f
@@ -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());
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -102,6 +102,7 @@ fn shell_command_for_invocation(invocation: &ToolInvocation) -> Option<(Vec<Stri
|
||||
let command = crate::tools::handlers::unified_exec::get_command(
|
||||
¶ms,
|
||||
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(
|
||||
¶ms,
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user