use anyhow::Context as _; use std::ffi::CString; use std::path::Path; use std::path::PathBuf; use tempfile::Builder; use tokio::process::Command; const CODEX_BUNDLE_IDENTIFIER: &str = "com.openai.codex"; const OPENAI_APPLE_TEAM_IDENTIFIER: &str = "2DC432GLL2"; const CODEX_DMG_URL_ARM64: &str = "https://persistent.oaistatic.com/codex-app-prod/Codex.dmg"; const CODEX_DMG_URL_X64: &str = "https://persistent.oaistatic.com/codex-app-prod/Codex-latest-x64.dmg"; pub async fn run_mac_app_open_or_install( workspace: PathBuf, download_url_override: Option, ) -> anyhow::Result<()> { if let Some(app_path) = find_existing_codex_app_path(&codex_app_search_dirs()) { eprintln!( "Opening Desktop app at {app_path}...", app_path = app_path.display() ); open_codex_app(&app_path, &workspace).await?; return Ok(()); } eprintln!("Desktop app not found; downloading installer..."); let download_url = download_url_override.unwrap_or_else(|| { let default_url = if is_apple_silicon_mac() { CODEX_DMG_URL_ARM64 } else { CODEX_DMG_URL_X64 }; default_url.to_string() }); let installed_app = download_and_install_codex_to_user_applications(&download_url) .await .context("failed to download/install Desktop app")?; eprintln!( "Launching Desktop app from {installed_app}...", installed_app = installed_app.display() ); open_codex_app(&installed_app, &workspace).await?; Ok(()) } fn is_apple_silicon_mac() -> bool { fn macos_sysctl_flag(name: &str) -> Option { let name = CString::new(name).ok()?; let mut value: libc::c_int = 0; let mut size = std::mem::size_of_val(&value); let result = unsafe { libc::sysctlbyname( name.as_ptr(), (&mut value as *mut libc::c_int).cast::(), &mut size, std::ptr::null_mut(), 0, ) }; (result == 0).then_some(value != 0) } std::env::consts::ARCH == "aarch64" || macos_sysctl_flag("sysctl.proc_translated").unwrap_or(false) || macos_sysctl_flag("hw.optional.arm64").unwrap_or(false) } fn find_existing_codex_app_path(applications_dirs: &[PathBuf]) -> Option { applications_dirs .iter() .flat_map(|dir| ["ChatGPT.app", "Codex.app"].map(|app_name| dir.join(app_name))) .find(|candidate| is_codex_app_bundle(candidate)) } fn codex_app_search_dirs() -> Vec { let mut paths = vec![PathBuf::from("/Applications")]; if let Some(home) = std::env::var_os("HOME") { paths.push(PathBuf::from(home).join("Applications")); } paths } fn is_codex_app_bundle(app_path: &Path) -> bool { if !app_path.is_dir() { return false; } std::process::Command::new("/usr/bin/plutil") .arg("-extract") .arg("CFBundleIdentifier") .arg("raw") .arg("-o") .arg("-") .arg(app_path.join("Contents/Info.plist")) .output() .is_ok_and(|output| { output.status.success() && String::from_utf8_lossy(&output.stdout).trim() == CODEX_BUNDLE_IDENTIFIER }) } async fn open_codex_app(app_path: &Path, workspace: &Path) -> anyhow::Result<()> { verify_codex_app_bundle(app_path).await?; eprintln!( "Opening workspace {workspace}...", workspace = workspace.display() ); let url = codex_new_thread_url(workspace); let status = Command::new("open") .arg("-a") .arg(app_path) .arg(&url) .status() .await .context("failed to invoke `open`")?; if status.success() { return Ok(()); } anyhow::bail!( "`open -a {app_path} {url}` exited with {status}", app_path = app_path.display(), url = url ); } async fn verify_codex_app_bundle(app_path: &Path) -> anyhow::Result<()> { let requirement = format!( "identifier \"{CODEX_BUNDLE_IDENTIFIER}\" and anchor apple generic and certificate leaf[subject.OU] = \"{OPENAI_APPLE_TEAM_IDENTIFIER}\"" ); let output = Command::new("/usr/bin/codesign") .args(["--verify", "--deep", "--strict"]) .arg(format!("-R={requirement}")) .arg(app_path) .output() .await .context("failed to verify Desktop app signature")?; if output.status.success() { return Ok(()); } anyhow::bail!( "Desktop app at {} failed OpenAI signature verification (team {OPENAI_APPLE_TEAM_IDENTIFIER}, bundle {CODEX_BUNDLE_IDENTIFIER}): {}", app_path.display(), String::from_utf8_lossy(&output.stderr).trim() ); } fn codex_new_thread_url(workspace: &Path) -> String { let workspace = workspace.as_os_str().to_string_lossy(); let mut serializer = url::form_urlencoded::Serializer::new(String::new()); serializer.append_pair("path", workspace.as_ref()); let query = serializer.finish(); format!("codex://threads/new?{query}") } async fn download_and_install_codex_to_user_applications(dmg_url: &str) -> anyhow::Result { let temp_dir = Builder::new() .prefix("codex-app-installer-") .tempdir() .context("failed to create temp dir")?; let tmp_root = temp_dir.path().to_path_buf(); let _temp_dir = temp_dir; let dmg_path = tmp_root.join("Codex.dmg"); download_dmg(dmg_url, &dmg_path).await?; eprintln!("Mounting Desktop app installer..."); let mount_point = mount_dmg(&dmg_path).await?; eprintln!( "Installer mounted at {mount_point}.", mount_point = mount_point.display() ); let result = async { let app_in_volume = find_codex_app_in_mount(&mount_point) .context("failed to locate Codex.app in mounted dmg")?; verify_codex_app_bundle(&app_in_volume) .await .context("refusing to install an unverified Desktop app")?; install_codex_app_bundle(&app_in_volume).await } .await; let detach_result = detach_dmg(&mount_point).await; if let Err(err) = detach_result { eprintln!( "warning: failed to detach dmg at {mount_point}: {err}", mount_point = mount_point.display() ); } result } async fn install_codex_app_bundle(app_in_volume: &Path) -> anyhow::Result { for applications_dir in candidate_applications_dirs()? { eprintln!( "Installing Desktop app into {applications_dir}...", applications_dir = applications_dir.display() ); std::fs::create_dir_all(&applications_dir).with_context(|| { format!( "failed to create applications dir {applications_dir}", applications_dir = applications_dir.display() ) })?; let dest_app = applications_dir.join("Codex.app"); if dest_app.is_dir() { return Ok(dest_app); } match copy_app_bundle(app_in_volume, &dest_app).await { Ok(()) => return Ok(dest_app), Err(err) => { eprintln!( "warning: failed to install Codex.app to {applications_dir}: {err}", applications_dir = applications_dir.display() ); } } } anyhow::bail!("failed to install Codex.app to any applications directory"); } fn candidate_applications_dirs() -> anyhow::Result> { let mut dirs = vec![PathBuf::from("/Applications")]; dirs.push(user_applications_dir()?); Ok(dirs) } async fn download_dmg(url: &str, dest: &Path) -> anyhow::Result<()> { eprintln!("Downloading installer..."); let status = Command::new("curl") .arg("-fL") .arg("--retry") .arg("3") .arg("--retry-delay") .arg("1") .arg("-o") .arg(dest) .arg(url) .status() .await .context("failed to invoke `curl`")?; if status.success() { return Ok(()); } anyhow::bail!("curl download failed with {status}"); } async fn mount_dmg(dmg_path: &Path) -> anyhow::Result { let output = Command::new("hdiutil") .arg("attach") .arg("-nobrowse") .arg("-readonly") .arg(dmg_path) .output() .await .context("failed to invoke `hdiutil attach`")?; if !output.status.success() { anyhow::bail!( "`hdiutil attach` failed with {status}: {stderr}", status = output.status, stderr = String::from_utf8_lossy(&output.stderr) ); } let stdout = String::from_utf8_lossy(&output.stdout); parse_hdiutil_attach_mount_point(&stdout) .map(PathBuf::from) .with_context(|| format!("failed to parse mount point from hdiutil output:\n{stdout}")) } async fn detach_dmg(mount_point: &Path) -> anyhow::Result<()> { let status = Command::new("hdiutil") .arg("detach") .arg(mount_point) .status() .await .context("failed to invoke `hdiutil detach`")?; if status.success() { return Ok(()); } anyhow::bail!("hdiutil detach failed with {status}"); } fn find_codex_app_in_mount(mount_point: &Path) -> anyhow::Result { let direct = mount_point.join("Codex.app"); if direct.is_dir() { return Ok(direct); } for entry in std::fs::read_dir(mount_point).with_context(|| { format!( "failed to read {mount_point}", mount_point = mount_point.display() ) })? { let entry = entry.context("failed to read mount directory entry")?; let path = entry.path(); if path.extension().is_some_and(|ext| ext == "app") && path.is_dir() { return Ok(path); } } anyhow::bail!( "no .app bundle found at {mount_point}", mount_point = mount_point.display() ); } async fn copy_app_bundle(src_app: &Path, dest_app: &Path) -> anyhow::Result<()> { let status = Command::new("ditto") .arg(src_app) .arg(dest_app) .status() .await .context("failed to invoke `ditto`")?; if status.success() { return Ok(()); } anyhow::bail!("ditto copy failed with {status}"); } fn user_applications_dir() -> anyhow::Result { let home = std::env::var_os("HOME").context("HOME is not set")?; Ok(PathBuf::from(home).join("Applications")) } fn parse_hdiutil_attach_mount_point(output: &str) -> Option { output.lines().find_map(|line| { if !line.contains("/Volumes/") { return None; } if let Some((_, mount)) = line.rsplit_once('\t') { return Some(mount.trim().to_string()); } line.split_whitespace() .find(|field| field.starts_with("/Volumes/")) .map(str::to_string) }) } #[cfg(test)] mod tests { use super::codex_new_thread_url; use super::find_existing_codex_app_path; use super::open_codex_app; use super::parse_hdiutil_attach_mount_point; use super::verify_codex_app_bundle; use pretty_assertions::assert_eq; use std::fs; use std::path::Path; fn write_app_bundle(app_path: &Path, bundle_identifier: &str) { let contents_path = app_path.join("Contents"); fs::create_dir_all(&contents_path).expect("create app bundle"); fs::write( contents_path.join("Info.plist"), format!( r#"CFBundleIdentifier{bundle_identifier}"# ), ) .expect("write Info.plist"); } #[test] fn finds_chatgpt_app_with_codex_bundle_identifier() { let temp_dir = tempfile::tempdir().expect("create temp dir"); let app_path = temp_dir.path().join("ChatGPT.app"); write_app_bundle(&app_path, "com.openai.codex"); assert_eq!( find_existing_codex_app_path(&[temp_dir.path().to_path_buf()]), Some(app_path) ); } #[test] fn ignores_classic_chatgpt_app() { let temp_dir = tempfile::tempdir().expect("create temp dir"); write_app_bundle(&temp_dir.path().join("ChatGPT.app"), "com.openai.chat"); let codex_app_path = temp_dir.path().join("Codex.app"); write_app_bundle(&codex_app_path, "com.openai.codex"); assert_eq!( find_existing_codex_app_path(&[temp_dir.path().to_path_buf()]), Some(codex_app_path) ); } #[tokio::test] async fn rejects_unsigned_app_with_codex_bundle_identifier() { let temp_dir = tempfile::tempdir().expect("create temp dir"); let app_path = temp_dir.path().join("Codex.app"); write_app_bundle(&app_path, "com.openai.codex"); let err = verify_codex_app_bundle(&app_path) .await .expect_err("unsigned app should not satisfy the OpenAI signing requirement"); assert!( err.to_string() .contains("failed OpenAI signature verification"), "unexpected verification error: {err}" ); } #[tokio::test] async fn rejects_valid_signature_without_openai_signing_identity() { let temp_dir = tempfile::tempdir().expect("create temp dir"); let app_path = temp_dir.path().join("Codex.app"); let contents_path = app_path.join("Contents"); let executable_path = contents_path.join("MacOS/Codex"); fs::create_dir_all(executable_path.parent().expect("executable parent")) .expect("create executable directory"); fs::copy("/usr/bin/true", &executable_path).expect("copy executable into app bundle"); fs::write( contents_path.join("Info.plist"), r#"CFBundleIdentifiercom.openai.codexCFBundleExecutableCodex"#, ) .expect("write Info.plist"); let sign_status = std::process::Command::new("/usr/bin/codesign") .args(["--force", "--sign", "-"]) .arg(&app_path) .status() .expect("sign app bundle"); assert!(sign_status.success(), "failed to ad-hoc sign app bundle"); let verify_status = std::process::Command::new("/usr/bin/codesign") .args(["--verify", "--deep", "--strict"]) .arg(&app_path) .status() .expect("verify app bundle signature"); assert!(verify_status.success(), "ad-hoc signature should be valid"); let err = verify_codex_app_bundle(&app_path) .await .expect_err("valid signature without the OpenAI team should not be trusted"); assert!( err.to_string() .contains("failed OpenAI signature verification"), "unexpected verification error: {err}" ); } #[tokio::test] async fn refuses_to_launch_unsigned_existing_codex_app() { let temp_dir = tempfile::tempdir().expect("create temp dir"); let app_path = temp_dir.path().join("Codex.app"); write_app_bundle(&app_path, "com.openai.codex"); let err = open_codex_app(&app_path, temp_dir.path()) .await .expect_err("unsigned existing app should not be launched"); assert!( err.to_string() .contains("failed OpenAI signature verification"), "unexpected launch error: {err}" ); } #[test] fn parses_mount_point_from_tab_separated_hdiutil_output() { let output = "/dev/disk2s1\tApple_HFS\tCodex\t/Volumes/Codex\n"; assert_eq!( parse_hdiutil_attach_mount_point(output).as_deref(), Some("/Volumes/Codex") ); } #[test] fn parses_mount_point_with_spaces() { let output = "/dev/disk2s1\tApple_HFS\tCodex Installer\t/Volumes/Codex Installer\n"; assert_eq!( parse_hdiutil_attach_mount_point(output).as_deref(), Some("/Volumes/Codex Installer") ); } #[test] fn codex_new_thread_url_encodes_workspace_path() { let url = url::Url::parse(&codex_new_thread_url(Path::new("/tmp/codex workspace/#1"))) .expect("deep link should parse"); assert_eq!( ( url.scheme().to_string(), url.host_str().map(str::to_string), url.path().to_string(), url.query_pairs().into_owned().collect::>(), ), ( "codex".to_string(), Some("threads".to_string()), "/new".to_string(), vec![("path".to_string(), "/tmp/codex workspace/#1".to_string())], ) ); } }