mirror of
https://github.com/openai/codex.git
synced 2026-11-07 20:30:33 +00:00
## Why Concurrent Linux test spawns can inherit writable descriptors for executable fixtures, preventing those fixtures from being executed even after the writing test closes its descriptor. ## What changed Replace direct executable fixture writes and copies across the Rust tests with `codex_utils_cargo_bin::write_executable` and `copy_executable`. These existing helpers complete writes and copies in separate processes on Linux. Remove the empty temporary inode before creating fake bubblewrap executables, and stage tampered bubblewrap bytes in a separate data file before copying them into the executable fixture. Add the required test dependencies. GitOrigin-RevId: c875c83088731a4933ef1574e8d79d8cecc12f54
514 lines
17 KiB
Rust
514 lines
17 KiB
Rust
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<String>,
|
|
) -> 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<bool> {
|
|
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::<libc::c_void>(),
|
|
&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<PathBuf> {
|
|
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<PathBuf> {
|
|
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<PathBuf> {
|
|
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<PathBuf> {
|
|
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<Vec<PathBuf>> {
|
|
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<PathBuf> {
|
|
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<PathBuf> {
|
|
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<PathBuf> {
|
|
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<String> {
|
|
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#"<?xml version="1.0"?><plist version="1.0"><dict><key>CFBundleIdentifier</key><string>{bundle_identifier}</string></dict></plist>"#
|
|
),
|
|
)
|
|
.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");
|
|
codex_utils_cargo_bin::copy_executable(Path::new("/usr/bin/true"), &executable_path)
|
|
.expect("copy executable into app bundle");
|
|
fs::write(
|
|
contents_path.join("Info.plist"),
|
|
r#"<?xml version="1.0"?><plist version="1.0"><dict><key>CFBundleIdentifier</key><string>com.openai.codex</string><key>CFBundleExecutable</key><string>Codex</string></dict></plist>"#,
|
|
)
|
|
.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::<Vec<_>>(),
|
|
),
|
|
(
|
|
"codex".to_string(),
|
|
Some("threads".to_string()),
|
|
"/new".to_string(),
|
|
vec![("path".to_string(), "/tmp/codex workspace/#1".to_string())],
|
|
)
|
|
);
|
|
}
|
|
}
|