package: include zsh fork in Codex package

Summary:
- add a checked-in codex-zsh DotSlash manifest for supported Unix targets
- fetch the matching zsh fork artifact during package builds and install it at codex-resources/zsh/bin/zsh when available
- expose the bundled zsh fork through install-context and use it as the runtime zsh path for packaged installs
- remove the user/profile zsh_path config override now that the fork is package-managed
- update app-server zsh-fork integration tests to spawn from a temporary package layout instead of configuring zsh_path directly
- use copyfile for package executable staging so local package smoke tests do not preserve platform-specific source metadata

Test Plan:
- just fmt
- just write-config-schema
- just fix -p codex-config -p codex-core -p codex-exec
- just fix -p codex-app-server
- python3 -m py_compile scripts/codex_package/cli.py scripts/codex_package/layout.py scripts/codex_package/zsh.py
- python3 -m unittest discover scripts/codex_package
- cargo test -p codex-core default_zsh_path_sets_runtime_zsh_path
- cargo test -p codex-core session_new_fails_when_zsh_fork_enabled_without_packaged_zsh
- cargo test -p codex-core restricted_read_implicitly_allows_helper_executables
- cargo test -p codex-config
- cargo test -p codex-app-server turn_start_shell_zsh_fork_executes_command_v2
- cargo test -p codex-app-server turn_start_shell_zsh_fork
This commit is contained in:
Michael Bolin
2026-05-22 15:46:31 -07:00
parent 7924743c38
commit c11e772789
27 changed files with 284 additions and 50 deletions
+3
View File
@@ -1905,6 +1905,7 @@ dependencies = [
"codex-git-utils",
"codex-guardian",
"codex-hooks",
"codex-install-context",
"codex-login",
"codex-mcp",
"codex-memories-extension",
@@ -2711,6 +2712,7 @@ dependencies = [
"codex-core",
"codex-feedback",
"codex-git-utils",
"codex-install-context",
"codex-login",
"codex-model-provider-info",
"codex-otel",
@@ -3143,6 +3145,7 @@ dependencies = [
"codex-core",
"codex-exec-server",
"codex-extension-api",
"codex-install-context",
"codex-login",
"codex-protocol",
"codex-shell-command",
+1
View File
@@ -45,6 +45,7 @@ codex-guardian = { workspace = true }
codex-git-utils = { workspace = true }
codex-file-watcher = { workspace = true }
codex-hooks = { workspace = true }
codex-install-context = { workspace = true }
codex-otel = { workspace = true }
codex-plugin = { workspace = true }
codex-shell-command = { workspace = true }
+20
View File
@@ -9,6 +9,7 @@ use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
use codex_exec_server::LOCAL_FS;
use codex_features::feature_for_key;
use codex_install_context::InstallContext;
use codex_login::AuthManager;
use codex_login::default_client::set_default_client_residency_requirement;
use codex_utils_absolute_path::AbsolutePathBuf;
@@ -159,6 +160,7 @@ impl ConfigManager {
.await?;
self.apply_runtime_feature_enablement(&mut config);
self.apply_arg0_paths(&mut config);
apply_default_zsh_path(&mut config);
Ok(config)
}
@@ -180,6 +182,7 @@ impl ConfigManager {
}
self.apply_runtime_feature_enablement(&mut config);
self.apply_arg0_paths(&mut config);
apply_default_zsh_path(&mut config);
Ok(config)
}
@@ -219,6 +222,11 @@ impl ConfigManager {
typesafe_overrides: ConfigOverrides,
fallback_cwd: Option<PathBuf>,
) -> std::io::Result<Config> {
let mut typesafe_overrides = typesafe_overrides;
if typesafe_overrides.default_zsh_path.is_none() {
typesafe_overrides.default_zsh_path = default_zsh_path();
}
let merged_cli_overrides = cli_overrides
.iter()
.cloned()
@@ -319,6 +327,18 @@ impl ConfigManager {
}
}
fn apply_default_zsh_path(config: &mut Config) {
if config.zsh_path.is_none() {
config.zsh_path = default_zsh_path();
}
}
fn default_zsh_path() -> Option<PathBuf> {
InstallContext::current()
.bundled_zsh_path()
.map(AbsolutePathBuf::into_path_buf)
}
pub(crate) fn protected_feature_keys(config_layer_stack: &ConfigLayerStack) -> BTreeSet<String> {
let mut protected_features = config_layer_stack
.effective_config()
@@ -173,6 +173,20 @@ impl McpProcess {
.await
}
pub async fn new_with_program_and_env(
codex_home: &Path,
program: &Path,
env_overrides: &[(&str, Option<&str>)],
) -> anyhow::Result<Self> {
Self::new_with_program_env_and_args(
codex_home,
program,
env_overrides,
&[DISABLE_PLUGIN_STARTUP_TASKS_ARG],
)
.await
}
async fn new_with_env_and_args(
codex_home: &Path,
env_overrides: &[(&str, Option<&str>)],
@@ -180,6 +194,15 @@ impl McpProcess {
) -> anyhow::Result<Self> {
let program = codex_utils_cargo_bin::cargo_bin("codex-app-server")
.context("should find binary for codex-app-server")?;
Self::new_with_program_env_and_args(codex_home, &program, env_overrides, args).await
}
async fn new_with_program_env_and_args(
codex_home: &Path,
program: &Path,
env_overrides: &[(&str, Option<&str>)],
args: &[&str],
) -> anyhow::Result<Self> {
let mut cmd = Command::new(program);
cmd.stdin(Stdio::piped());
@@ -37,6 +37,7 @@ use core_test_support::skip_if_no_network;
use pretty_assertions::assert_eq;
use std::collections::BTreeMap;
use std::path::Path;
use std::path::PathBuf;
use tempfile::TempDir;
use tokio::time::timeout;
@@ -94,10 +95,9 @@ async fn turn_start_shell_zsh_fork_executes_command_v2() -> Result<()> {
(Feature::UnifiedExec, false),
(Feature::ShellSnapshot, false),
]),
&zsh_path,
)?;
let mut mcp = create_zsh_test_mcp_process(&codex_home, &workspace).await?;
let mut mcp = create_zsh_test_mcp_process(&codex_home, &workspace, &zsh_path).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let start_id = mcp
@@ -162,7 +162,7 @@ async fn turn_start_shell_zsh_fork_executes_command_v2() -> Result<()> {
};
assert_eq!(id, "call-zsh-fork");
assert_eq!(status, CommandExecutionStatus::InProgress);
assert!(command.starts_with(&zsh_path.display().to_string()));
assert!(command.starts_with(&packaged_zsh_path(&codex_home).display().to_string()));
assert!(command.contains("/bin/sh -c"));
assert!(command.contains("sleep 0.01"));
assert!(command.contains(&release_marker.display().to_string()));
@@ -213,10 +213,9 @@ async fn turn_start_shell_zsh_fork_exec_approval_decline_v2() -> Result<()> {
(Feature::UnifiedExec, false),
(Feature::ShellSnapshot, false),
]),
&zsh_path,
)?;
let mut mcp = create_zsh_test_mcp_process(&codex_home, &workspace).await?;
let mut mcp = create_zsh_test_mcp_process(&codex_home, &workspace, &zsh_path).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let start_id = mcp
@@ -346,10 +345,9 @@ async fn turn_start_shell_zsh_fork_exec_approval_cancel_v2() -> Result<()> {
(Feature::UnifiedExec, false),
(Feature::ShellSnapshot, false),
]),
&zsh_path,
)?;
let mut mcp = create_zsh_test_mcp_process(&codex_home, &workspace).await?;
let mut mcp = create_zsh_test_mcp_process(&codex_home, &workspace, &zsh_path).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let start_id = mcp
@@ -505,10 +503,9 @@ async fn turn_start_shell_zsh_fork_subcommand_decline_marks_parent_declined_v2()
(Feature::UnifiedExec, false),
(Feature::ShellSnapshot, false),
]),
&zsh_path,
)?;
let mut mcp = create_zsh_test_mcp_process(&codex_home, &workspace).await?;
let mut mcp = create_zsh_test_mcp_process(&codex_home, &workspace, &zsh_path).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
let start_id = mcp
@@ -603,7 +600,8 @@ async fn turn_start_shell_zsh_fork_subcommand_decline_marks_parent_declined_v2()
);
approved_subcommand_strings.push(approval_command.to_string());
}
let is_parent_approval = approval_command.contains(&zsh_path.display().to_string())
let is_parent_approval = approval_command
.contains(&packaged_zsh_path(&codex_home).display().to_string())
&& (approval_command.contains(&shell_command)
|| (has_first_file && has_second_file)
|| approval_command.contains(&parent_shell_hint));
@@ -738,9 +736,53 @@ async fn turn_start_shell_zsh_fork_subcommand_decline_marks_parent_declined_v2()
Ok(())
}
async fn create_zsh_test_mcp_process(codex_home: &Path, zdotdir: &Path) -> Result<McpProcess> {
async fn create_zsh_test_mcp_process(
codex_home: &Path,
zdotdir: &Path,
zsh_path: &Path,
) -> Result<McpProcess> {
let app_server = create_test_package_app_server(codex_home, zsh_path)?;
let zdotdir = zdotdir.to_string_lossy().into_owned();
McpProcess::new_with_env(codex_home, &[("ZDOTDIR", Some(zdotdir.as_str()))]).await
McpProcess::new_with_program_and_env(
codex_home,
&app_server,
&[("ZDOTDIR", Some(zdotdir.as_str()))],
)
.await
}
fn create_test_package_app_server(codex_home: &Path, zsh_path: &Path) -> Result<PathBuf> {
let package_dir = codex_home.join("test-package");
let bin_dir = package_dir.join("bin");
let package_zsh_path = packaged_zsh_path(codex_home);
let Some(zsh_bin_dir) = package_zsh_path.parent() else {
anyhow::bail!("packaged zsh path should have parent");
};
std::fs::create_dir_all(&bin_dir)?;
std::fs::create_dir_all(zsh_bin_dir)?;
std::fs::write(package_dir.join("codex-package.json"), "{}")?;
let app_server = bin_dir.join("codex-app-server");
copy_with_permissions(
&codex_utils_cargo_bin::cargo_bin("codex-app-server")?,
&app_server,
)?;
copy_with_permissions(zsh_path, &package_zsh_path)?;
Ok(app_server)
}
fn packaged_zsh_path(codex_home: &Path) -> PathBuf {
codex_home
.join("test-package")
.join("codex-resources")
.join("zsh")
.join("bin")
.join("zsh")
}
fn copy_with_permissions(source: &Path, destination: &Path) -> std::io::Result<()> {
std::fs::copy(source, destination)?;
std::fs::set_permissions(destination, std::fs::metadata(source)?.permissions())
}
fn create_config_toml(
@@ -748,7 +790,6 @@ fn create_config_toml(
server_uri: &str,
approval_policy: &str,
feature_flags: &BTreeMap<Feature, bool>,
zsh_path: &Path,
) -> std::io::Result<()> {
let mut features = BTreeMap::from([(Feature::RemoteModels, false)]);
for (feature, enabled) in feature_flags {
@@ -774,7 +815,6 @@ fn create_config_toml(
model = "mock-model"
approval_policy = "{approval_policy}"
sandbox_mode = "read-only"
zsh_path = "{zsh_path}"
model_provider = "mock_provider"
@@ -787,9 +827,7 @@ base_url = "{server_uri}/v1"
wire_api = "responses"
request_max_retries = 0
stream_max_retries = 0
"#,
approval_policy = approval_policy,
zsh_path = zsh_path.display()
"#
),
)
}
+1
View File
@@ -506,6 +506,7 @@ fn config_overrides_from_interactive(
codex_self_exe: arg0_paths.codex_self_exe.clone(),
codex_linux_sandbox_exe: arg0_paths.codex_linux_sandbox_exe.clone(),
main_execve_wrapper_exe: arg0_paths.main_execve_wrapper_exe.clone(),
default_zsh_path: crate::default_zsh_path(),
show_raw_agent_reasoning: interactive.oss.then_some(true),
additional_writable_roots: interactive.add_dir.clone(),
..Default::default()
+8
View File
@@ -71,6 +71,7 @@ use codex_core::config::resolve_profile_v2_config_path;
use codex_features::FEATURES;
use codex_features::Stage;
use codex_features::is_known_feature_key;
use codex_install_context::InstallContext;
use codex_login::AuthManager;
use codex_login::CodexAuth;
use codex_login::read_codex_access_token_from_env;
@@ -1557,6 +1558,12 @@ async fn load_exec_server_config(
.await?)
}
pub(crate) fn default_zsh_path() -> Option<PathBuf> {
InstallContext::current()
.bundled_zsh_path()
.map(AbsolutePathBuf::into_path_buf)
}
async fn load_exec_server_remote_auth(
config: &codex_core::config::Config,
missing_auth_error: &'static str,
@@ -1681,6 +1688,7 @@ async fn run_debug_prompt_input_command(
codex_self_exe: arg0_paths.codex_self_exe,
codex_linux_sandbox_exe: arg0_paths.codex_linux_sandbox_exe,
main_execve_wrapper_exe: arg0_paths.main_execve_wrapper_exe,
default_zsh_path: default_zsh_path(),
show_raw_agent_reasoning: shared.oss.then_some(true),
ephemeral: Some(true),
bypass_hook_trust: shared.bypass_hook_trust.then_some(true),
-3
View File
@@ -291,9 +291,6 @@ pub struct ConfigToml {
#[schemars(skip)]
pub js_repl_node_module_dirs: Option<Vec<AbsolutePathBuf>>,
/// Optional absolute path to patched zsh used by zsh-exec-bridge-backed shell execution.
pub zsh_path: Option<AbsolutePathBuf>,
/// Profile to use from the `profiles` map.
pub profile: Option<String>,
-2
View File
@@ -48,8 +48,6 @@ pub struct ConfigProfile {
/// Deprecated: ignored.
#[schemars(skip)]
pub js_repl_node_module_dirs: Option<Vec<AbsolutePathBuf>>,
/// Optional absolute path to patched zsh used by zsh-exec-bridge-backed shell execution.
pub zsh_path: Option<AbsolutePathBuf>,
pub experimental_compact_prompt_file: Option<AbsolutePathBuf>,
pub include_permissions_instructions: Option<bool>,
pub include_apps_instructions: Option<bool>,
-16
View File
@@ -711,14 +711,6 @@
}
],
"default": null
},
"zsh_path": {
"allOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
}
],
"description": "Optional absolute path to patched zsh used by zsh-exec-bridge-backed shell execution."
}
},
"type": "object"
@@ -4986,14 +4978,6 @@
],
"default": null,
"description": "Windows-specific configuration."
},
"zsh_path": {
"allOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
}
],
"description": "Optional absolute path to patched zsh used by zsh-exec-bridge-backed shell execution."
}
},
"title": "ConfigToml",
+19
View File
@@ -4470,6 +4470,25 @@ async fn add_dir_override_extends_workspace_writable_roots() -> std::io::Result<
Ok(())
}
#[tokio::test]
async fn default_zsh_path_sets_runtime_zsh_path() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
let default_zsh_path = codex_home.path().join("packaged-zsh");
let config = Config::load_from_base_config_with_overrides(
ConfigToml::default(),
ConfigOverrides {
default_zsh_path: Some(default_zsh_path.clone()),
..Default::default()
},
codex_home.abs(),
)
.await?;
assert_eq!(config.zsh_path, Some(default_zsh_path));
Ok(())
}
#[tokio::test]
async fn sqlite_home_defaults_to_codex_home_for_workspace_write() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
+4 -3
View File
@@ -1397,6 +1397,7 @@ impl Config {
cfg,
ConfigOverrides {
cwd: Some(self.cwd.to_path_buf()),
default_zsh_path: refreshed_config.zsh_path.clone(),
..Default::default()
},
refreshed_config.codex_home.clone(),
@@ -2128,7 +2129,7 @@ pub struct ConfigOverrides {
pub codex_self_exe: Option<PathBuf>,
pub codex_linux_sandbox_exe: Option<PathBuf>,
pub main_execve_wrapper_exe: Option<PathBuf>,
pub zsh_path: Option<PathBuf>,
pub default_zsh_path: Option<PathBuf>,
pub base_instructions: Option<String>,
pub developer_instructions: Option<String>,
pub personality: Option<Personality>,
@@ -2480,7 +2481,7 @@ impl Config {
codex_self_exe,
codex_linux_sandbox_exe,
main_execve_wrapper_exe,
zsh_path: zsh_path_override,
default_zsh_path,
base_instructions,
developer_instructions,
personality,
@@ -3199,7 +3200,7 @@ impl Config {
)
.await?;
let compact_prompt = compact_prompt.or(file_compact_prompt);
let zsh_path = zsh_path_override.or(cfg.zsh_path.map(Into::into));
let zsh_path = default_zsh_path;
let review_model = override_review_model.or(cfg.review_model);
@@ -82,7 +82,7 @@ async fn restricted_read_implicitly_allows_helper_executables() -> std::io::Resu
},
ConfigOverrides {
cwd: Some(cwd.clone()),
zsh_path: Some(zsh_path.clone()),
default_zsh_path: Some(zsh_path.clone()),
main_execve_wrapper_exe: Some(execve_wrapper),
..Default::default()
},
+2 -2
View File
@@ -826,13 +826,13 @@ impl Session {
} else if use_zsh_fork_shell {
let zsh_path = config.zsh_path.as_ref().ok_or_else(|| {
anyhow::anyhow!(
"zsh fork feature enabled, but `zsh_path` is not configured; set `zsh_path` in config.toml"
"zsh fork feature enabled, but no packaged zsh fork is available for this install"
)
})?;
let zsh_path = zsh_path.to_path_buf();
shell::get_shell(shell::ShellType::Zsh, Some(&zsh_path)).ok_or_else(|| {
anyhow::anyhow!(
"zsh fork feature enabled, but zsh_path `{}` is not usable; set `zsh_path` to a valid zsh executable",
"zsh fork feature enabled, but packaged zsh fork `{}` is not usable",
zsh_path.display()
)
})?
+2 -2
View File
@@ -4319,7 +4319,7 @@ async fn absolute_cwd_update_with_turn_environment_is_allowed() {
}
#[tokio::test]
async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() {
async fn session_new_fails_when_zsh_fork_enabled_without_packaged_zsh() {
let codex_home = tempfile::tempdir().expect("create temp dir");
let mut config = build_test_config(codex_home.path()).await;
config
@@ -4420,7 +4420,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() {
Err(err) => err,
};
let msg = format!("{err:#}");
assert!(msg.contains("zsh fork feature enabled, but `zsh_path` is not configured"));
assert!(msg.contains("zsh fork feature enabled, but no packaged zsh fork is available"));
}
// todo: use online model info
+1
View File
@@ -32,6 +32,7 @@ codex-config = { workspace = true }
codex-core = { workspace = true }
codex-feedback = { workspace = true }
codex-git-utils = { workspace = true }
codex-install-context = { workspace = true }
codex-login = { workspace = true }
codex-model-provider-info = { workspace = true }
codex-otel = { workspace = true }
+4 -1
View File
@@ -70,6 +70,7 @@ use codex_core::format_exec_policy_error_with_source;
use codex_core::path_utils;
use codex_feedback::CodexFeedback;
use codex_git_utils::get_git_repo_root;
use codex_install_context::InstallContext;
use codex_login::AuthConfig;
use codex_login::default_client::set_default_client_residency_requirement;
use codex_login::default_client::set_default_originator;
@@ -416,7 +417,9 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
codex_self_exe: arg0_paths.codex_self_exe.clone(),
codex_linux_sandbox_exe: arg0_paths.codex_linux_sandbox_exe.clone(),
main_execve_wrapper_exe: arg0_paths.main_execve_wrapper_exe.clone(),
zsh_path: None,
default_zsh_path: InstallContext::current()
.bundled_zsh_path()
.map(AbsolutePathBuf::into_path_buf),
base_instructions: None,
developer_instructions: None,
personality: None,
+35
View File
@@ -11,6 +11,7 @@ const PATH_DIRNAME: &str = "codex-path";
const RELEASES_DIRNAME: &str = "releases";
const RESOURCES_DIRNAME: &str = "codex-resources";
const STANDALONE_PACKAGES_DIRNAME: &str = "standalone";
const ZSH_DIRNAME: &str = "zsh";
static INSTALL_CONTEXT: OnceLock<InstallContext> = OnceLock::new();
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -166,6 +167,18 @@ impl InstallContext {
None
}
pub fn bundled_zsh_path(&self) -> Option<AbsolutePathBuf> {
if cfg!(windows) {
None
} else {
self.bundled_resource(zsh_resource_path())
}
}
pub fn bundled_zsh_bin_dir(&self) -> Option<AbsolutePathBuf> {
self.bundled_zsh_path()?.parent()
}
}
impl CodexPackageLayout {
@@ -260,6 +273,10 @@ fn default_rg_command() -> PathBuf {
}
}
fn zsh_resource_path() -> PathBuf {
PathBuf::from(ZSH_DIRNAME).join(BIN_DIRNAME).join("zsh")
}
#[cfg(test)]
mod tests {
use super::*;
@@ -345,6 +362,11 @@ mod tests {
fs::write(&exe_path, "")?;
fs::write(resources_dir.join(TEST_RESOURCE_NAME), "")?;
fs::write(path_dir.join(default_rg_command()), "")?;
if !cfg!(windows) {
let zsh_path = resources_dir.join(zsh_resource_path());
fs::create_dir_all(zsh_path.parent().expect("zsh path should have parent"))?;
fs::write(&zsh_path, "")?;
}
let canonical_package_dir =
AbsolutePathBuf::from_absolute_path(package_dir.path().canonicalize()?)?;
let canonical_bin_dir = AbsolutePathBuf::from_absolute_path(bin_dir.canonicalize()?)?;
@@ -382,6 +404,19 @@ mod tests {
context.bundled_resource(TEST_RESOURCE_NAME),
Some(canonical_resources_dir.join(TEST_RESOURCE_NAME))
);
if cfg!(windows) {
assert_eq!(context.bundled_zsh_path(), None);
assert_eq!(context.bundled_zsh_bin_dir(), None);
} else {
assert_eq!(
context.bundled_zsh_path(),
Some(canonical_resources_dir.join(zsh_resource_path()))
);
assert_eq!(
context.bundled_zsh_bin_dir(),
Some(canonical_resources_dir.join(ZSH_DIRNAME).join(BIN_DIRNAME))
);
}
Ok(())
}
+2 -1
View File
@@ -23,8 +23,10 @@ codex-config = { workspace = true }
codex-core = { workspace = true }
codex-exec-server = { workspace = true }
codex-extension-api = { workspace = true }
codex-install-context = { workspace = true }
codex-login = { workspace = true }
codex-protocol = { workspace = true }
codex-utils-absolute-path = { workspace = true }
codex-utils-cli = { workspace = true }
codex-utils-json-to-toml = { workspace = true }
rmcp = { workspace = true }
@@ -43,7 +45,6 @@ tracing = { workspace = true, features = ["log"] }
tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] }
[dev-dependencies]
codex-utils-absolute-path = { workspace = true }
codex-shell-command = { workspace = true }
core_test_support = { workspace = true }
mcp_test_support = { workspace = true }
@@ -4,6 +4,7 @@ use codex_arg0::Arg0DispatchPaths;
use codex_core::config::Config;
use codex_core::config::ConfigBuilder;
use codex_core::config::ConfigOverrides;
use codex_install_context::InstallContext;
use codex_protocol::ThreadId;
use codex_protocol::config_types::SandboxMode;
use codex_protocol::protocol::AskForApproval;
@@ -175,6 +176,9 @@ impl CodexToolCallParam {
codex_self_exe: arg0_paths.codex_self_exe.clone(),
codex_linux_sandbox_exe: arg0_paths.codex_linux_sandbox_exe.clone(),
main_execve_wrapper_exe: arg0_paths.main_execve_wrapper_exe.clone(),
default_zsh_path: InstallContext::current()
.bundled_zsh_path()
.map(codex_utils_absolute_path::AbsolutePathBuf::into_path_buf),
base_instructions,
developer_instructions,
compact_prompt,
+7
View File
@@ -1851,6 +1851,13 @@ async fn load_config_or_exit_with_fallback_cwd(
strict_config: bool,
fallback_cwd: Option<PathBuf>,
) -> Config {
let mut overrides = overrides;
if overrides.default_zsh_path.is_none() {
overrides.default_zsh_path = codex_install_context::InstallContext::current()
.bundled_zsh_path()
.map(AbsolutePathBuf::into_path_buf);
}
#[allow(clippy::print_stderr)]
match ConfigBuilder::default()
.cli_overrides(cli_kv_overrides)
+7
View File
@@ -13,6 +13,7 @@ The builder creates a canonical Codex package directory:
│ └── <entrypoint>[.exe]
├── codex-resources
│ ├── bwrap # Linux only
│ ├── zsh/bin/zsh # supported Unix targets only
│ ├── codex-command-runner.exe # Windows only
│ └── codex-windows-sandbox-setup.exe # Windows only
└── codex-path
@@ -67,3 +68,9 @@ DotSlash manifest at `scripts/codex_package/rg`. Downloaded archives are cached
under `$TMPDIR/codex-package/<target>-rg` and are reused only after the recorded
size and SHA-256 digest have been verified. Pass `--rg-bin` to use a local
ripgrep executable instead.
The patched zsh fork used by `shell_zsh_fork` is fetched from the DotSlash
manifest at `scripts/codex_package/codex-zsh` when the selected target has a
matching prebuilt artifact. Downloaded archives are cached under
`$TMPDIR/codex-package/<target>-zsh` and installed at
`codex-resources/zsh/bin/zsh`.
+3 -1
View File
@@ -15,6 +15,7 @@ from .targets import TARGET_SPECS
from .targets import PackageInputs
from .targets import default_target
from .targets import resolve_input_path
from .zsh import resolve_zsh_bin
from .version import read_workspace_version
@@ -161,13 +162,14 @@ def main() -> int:
inputs = PackageInputs(
entrypoint_bin=source_outputs.entrypoint_bin,
rg_bin=resolve_rg_bin(spec, args.rg_bin),
zsh_bin=resolve_zsh_bin(spec),
bwrap_bin=source_outputs.bwrap_bin,
codex_command_runner_bin=source_outputs.codex_command_runner_bin,
codex_windows_sandbox_setup_bin=source_outputs.codex_windows_sandbox_setup_bin,
)
prepare_package_dir(package_dir, force=args.force)
build_package_dir(package_dir, version, variant, spec, inputs)
validate_package_dir(package_dir, variant, spec)
validate_package_dir(package_dir, variant, spec, include_zsh=inputs.zsh_bin is not None)
for archive_output in args.archive_output:
archive_path = archive_output.resolve()
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env dotslash
{
"name": "codex-zsh",
"platforms": {
"macos-aarch64": {
"size": 358776,
"hash": "sha256",
"digest": "c6dbb063a0135b947ab1cacc655b2b750874699472f412ec7daba97543a90c3c",
"format": "tar.gz",
"path": "codex-zsh/bin/zsh",
"providers": [
{
"url": "https://github.com/openai/codex/releases/download/rust-v0.132.0/codex-zsh-aarch64-apple-darwin.tar.gz"
}
]
},
"linux-x86_64": {
"size": 433413,
"hash": "sha256",
"digest": "5f42d9fc8e9c8c399a727512002906006ae9de966ea7b3d87ca36b47efc59938",
"format": "tar.gz",
"path": "codex-zsh/bin/zsh",
"providers": [
{
"url": "https://github.com/openai/codex/releases/download/rust-v0.132.0/codex-zsh-x86_64-unknown-linux-musl.tar.gz"
}
]
},
"linux-aarch64": {
"size": 411653,
"hash": "sha256",
"digest": "6c6e32c297425db02b4dbffb10925895875d14647fc3eb2f18767be97dc6a945",
"format": "tar.gz",
"path": "codex-zsh/bin/zsh",
"providers": [
{
"url": "https://github.com/openai/codex/releases/download/rust-v0.132.0/codex-zsh-aarch64-unknown-linux-musl.tar.gz"
}
]
}
}
}
+16 -1
View File
@@ -8,6 +8,7 @@ from pathlib import Path
from .targets import PackageInputs
from .targets import PackageVariant
from .targets import TargetSpec
from .zsh import ZSH_RESOURCE_PATH
LAYOUT_VERSION = 1
@@ -50,6 +51,13 @@ def build_package_dir(
)
copy_executable(inputs.rg_bin, path_dir / spec.rg_name, is_windows=spec.is_windows)
if inputs.zsh_bin is not None:
copy_executable(
inputs.zsh_bin,
resources_dir / ZSH_RESOURCE_PATH,
is_windows=False,
)
if inputs.bwrap_bin is not None:
copy_executable(inputs.bwrap_bin, resources_dir / "bwrap", is_windows=False)
@@ -83,6 +91,8 @@ def validate_package_dir(
package_dir: Path,
variant: PackageVariant,
spec: TargetSpec,
*,
include_zsh: bool,
) -> None:
required_dirs = [
Path("bin"),
@@ -122,6 +132,11 @@ def validate_package_dir(
]
executable_files = list(required_files)
if include_zsh:
zsh_path = Path("codex-resources") / ZSH_RESOURCE_PATH
required_files.append(zsh_path)
executable_files.append(zsh_path)
if spec.is_linux:
required_files.append(Path("codex-resources") / "bwrap")
executable_files.append(Path("codex-resources") / "bwrap")
@@ -148,7 +163,7 @@ def validate_package_dir(
def copy_executable(src: Path, dest: Path, *, is_windows: bool) -> None:
dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src, dest)
shutil.copyfile(src, dest)
if not is_windows:
mode = dest.stat().st_mode
dest.chmod(mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
+1
View File
@@ -40,6 +40,7 @@ class PackageVariant:
class PackageInputs:
entrypoint_bin: Path
rg_bin: Path
zsh_bin: Path | None
bwrap_bin: Path | None
codex_command_runner_bin: Path | None
codex_windows_sandbox_setup_bin: Path | None
+22
View File
@@ -0,0 +1,22 @@
"""Fetch the patched zsh fork used by shell_zsh_fork."""
from pathlib import Path
from .dotslash import fetch_dotslash_executable
from .targets import REPO_ROOT
from .targets import TargetSpec
ZSH_MANIFEST = REPO_ROOT / "scripts" / "codex_package" / "codex-zsh"
ZSH_RESOURCE_PATH = Path("zsh") / "bin" / "zsh"
def resolve_zsh_bin(spec: TargetSpec) -> Path | None:
return fetch_dotslash_executable(
spec,
manifest_path=ZSH_MANIFEST,
artifact_label="codex-zsh",
cache_key=f"{spec.target}-zsh",
dest_name="zsh",
missing_ok=True,
)