mirror of
https://github.com/openai/codex.git
synced 2026-11-12 21:20:34 +00:00
share trusted Desktop distribution discovery
This commit is contained in:
Generated
+11
@@ -2291,6 +2291,7 @@ dependencies = [
|
||||
"codex-config",
|
||||
"codex-core",
|
||||
"codex-core-plugins",
|
||||
"codex-desktop-distribution",
|
||||
"codex-exec",
|
||||
"codex-exec-server",
|
||||
"codex-execpolicy",
|
||||
@@ -2782,6 +2783,16 @@ dependencies = [
|
||||
"zip 2.4.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-desktop-distribution"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"codex-utils-absolute-path",
|
||||
"pretty_assertions",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-exec"
|
||||
version = "0.0.0"
|
||||
|
||||
@@ -33,6 +33,7 @@ members = [
|
||||
"connectors",
|
||||
"config",
|
||||
"context-fragments",
|
||||
"desktop-distribution",
|
||||
"shell-command",
|
||||
"shell-escalation",
|
||||
"skills",
|
||||
@@ -165,6 +166,7 @@ codex-home = { path = "codex-home" }
|
||||
codex-config = { path = "config" }
|
||||
codex-connectors = { path = "connectors" }
|
||||
codex-context-fragments = { path = "context-fragments" }
|
||||
codex-desktop-distribution = { path = "desktop-distribution" }
|
||||
codex-core = { path = "core" }
|
||||
codex-core-api = { path = "core-api" }
|
||||
codex-core-plugins = { path = "core-plugins" }
|
||||
|
||||
@@ -31,6 +31,7 @@ codex-chatgpt = { workspace = true }
|
||||
codex-cloud-tasks = { path = "../cloud-tasks" }
|
||||
codex-utils-cli = { workspace = true }
|
||||
codex-config = { workspace = true }
|
||||
codex-desktop-distribution = { workspace = true }
|
||||
codex-core = { workspace = true }
|
||||
codex-core-plugins = { workspace = true }
|
||||
codex-home = { workspace = true }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use anyhow::Context as _;
|
||||
use codex_desktop_distribution::discover_installed_distribution;
|
||||
use std::ffi::CString;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
@@ -13,7 +14,7 @@ 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() {
|
||||
if let Some(app_path) = find_existing_codex_app_path().await? {
|
||||
eprintln!(
|
||||
"Opening Codex Desktop at {app_path}...",
|
||||
app_path = app_path.display()
|
||||
@@ -63,18 +64,11 @@ fn is_apple_silicon_mac() -> bool {
|
||||
|| macos_sysctl_flag("hw.optional.arm64").unwrap_or(false)
|
||||
}
|
||||
|
||||
fn find_existing_codex_app_path() -> Option<PathBuf> {
|
||||
candidate_codex_app_paths()
|
||||
.into_iter()
|
||||
.find(|candidate| candidate.is_dir())
|
||||
}
|
||||
|
||||
fn candidate_codex_app_paths() -> Vec<PathBuf> {
|
||||
let mut paths = vec![PathBuf::from("/Applications/Codex.app")];
|
||||
if let Some(home) = std::env::var_os("HOME") {
|
||||
paths.push(PathBuf::from(home).join("Applications").join("Codex.app"));
|
||||
}
|
||||
paths
|
||||
async fn find_existing_codex_app_path() -> anyhow::Result<Option<PathBuf>> {
|
||||
let distribution = tokio::task::spawn_blocking(discover_installed_distribution)
|
||||
.await
|
||||
.context("Desktop discovery task failed")??;
|
||||
Ok(distribution.map(|distribution| distribution.app_root().to_path_buf()))
|
||||
}
|
||||
|
||||
async fn open_codex_app(app_path: &Path, workspace: &Path) -> anyhow::Result<()> {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use anyhow::Context as _;
|
||||
use codex_desktop_distribution::discover_installed_distribution;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use tokio::process::Command;
|
||||
@@ -31,19 +32,10 @@ pub async fn run_windows_app_open_or_install(
|
||||
}
|
||||
|
||||
async fn codex_app_is_installed() -> anyhow::Result<bool> {
|
||||
let output = Command::new("powershell.exe")
|
||||
.arg("-NoProfile")
|
||||
.arg("-Command")
|
||||
.arg("Get-StartApps -Name 'Codex' | Select-Object -First 1 -ExpandProperty AppID")
|
||||
.output()
|
||||
Ok(tokio::task::spawn_blocking(discover_installed_distribution)
|
||||
.await
|
||||
.context("failed to invoke `powershell.exe`")?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
Ok(!String::from_utf8_lossy(&output.stdout).trim().is_empty())
|
||||
.context("Desktop discovery task failed")??
|
||||
.is_some())
|
||||
}
|
||||
|
||||
async fn open_url(url: &str) -> anyhow::Result<()> {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "desktop-distribution",
|
||||
crate_name = "codex_desktop_distribution",
|
||||
)
|
||||
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "codex-desktop-distribution"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "codex_desktop_distribution"
|
||||
path = "src/lib.rs"
|
||||
doctest = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
@@ -0,0 +1,187 @@
|
||||
//! Locates the trusted Codex Desktop resources bundled with the installed app.
|
||||
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::Component;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[path = "macos.rs"]
|
||||
mod platform;
|
||||
#[cfg(not(any(target_os = "macos", windows)))]
|
||||
#[path = "unsupported.rs"]
|
||||
mod platform;
|
||||
#[cfg(windows)]
|
||||
#[path = "windows.rs"]
|
||||
mod platform;
|
||||
|
||||
// Resource override for trusted Desktop development builds and external CLI launchers.
|
||||
const DESKTOP_RESOURCES_PATH_ENV_VAR: &str = "CODEX_DESKTOP_RESOURCES_PATH";
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum DesktopDistributionError {
|
||||
/// No launcher hint was provided and no installed Codex Desktop app was found. This can
|
||||
/// happen, for example, when Desktop was uninstalled after installing a bundled plugin.
|
||||
#[error("no installed Codex Desktop distribution was found")]
|
||||
NotFound,
|
||||
/// The platform app-discovery mechanism could not be queried successfully.
|
||||
#[error("Codex Desktop discovery failed: {0}")]
|
||||
Discovery(String),
|
||||
/// An expected app resource could not be read or canonicalized.
|
||||
#[error("Codex Desktop filesystem validation failed during {stage}: {source}")]
|
||||
Filesystem {
|
||||
stage: &'static str,
|
||||
#[source]
|
||||
source: io::Error,
|
||||
},
|
||||
/// A requested resource was absolute, contained `..`, escaped through a symlink, or had the
|
||||
/// wrong file kind, so it was not strictly contained beneath Desktop's resources directory.
|
||||
#[error("Codex Desktop resource containment failed: {0}")]
|
||||
Containment(String),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DesktopResources {
|
||||
root: AbsolutePathBuf,
|
||||
}
|
||||
|
||||
impl DesktopResources {
|
||||
pub fn contained_file(
|
||||
&self,
|
||||
relative_path: impl AsRef<Path>,
|
||||
) -> Result<AbsolutePathBuf, DesktopDistributionError> {
|
||||
contained_path(&self.root, relative_path.as_ref(), ResourceKind::File)
|
||||
}
|
||||
|
||||
pub fn contained_directory(
|
||||
&self,
|
||||
relative_path: impl AsRef<Path>,
|
||||
) -> Result<AbsolutePathBuf, DesktopDistributionError> {
|
||||
contained_path(&self.root, relative_path.as_ref(), ResourceKind::Directory)
|
||||
}
|
||||
|
||||
/// Uses a resources directory supplied by the trusted Desktop launcher.
|
||||
pub fn from_trusted_path(root: PathBuf) -> Result<Self, DesktopDistributionError> {
|
||||
let root = canonical(&root, "resources root")?;
|
||||
if !root.as_path().is_dir() {
|
||||
return Err(containment("expected the resources root to be a directory"));
|
||||
}
|
||||
Ok(Self { root })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct InstalledDesktop {
|
||||
app_root: AbsolutePathBuf,
|
||||
resources: DesktopResources,
|
||||
}
|
||||
|
||||
impl InstalledDesktop {
|
||||
pub fn app_root(&self) -> &Path {
|
||||
self.app_root.as_path()
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "macos", windows, test))]
|
||||
fn from_paths(
|
||||
app_root: PathBuf,
|
||||
resources_root: PathBuf,
|
||||
) -> Result<Self, DesktopDistributionError> {
|
||||
let app_root = canonical(&app_root, "application root")?;
|
||||
let resources = DesktopResources::from_trusted_path(resources_root)?;
|
||||
if resources.root == app_root || !resources.root.as_path().starts_with(app_root.as_path()) {
|
||||
return Err(containment(
|
||||
"resources root is not strictly below the discovered application root",
|
||||
));
|
||||
}
|
||||
Ok(Self {
|
||||
app_root,
|
||||
resources,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Uses resources supplied by Desktop, otherwise discovers the installed stable app.
|
||||
pub fn locate_current_or_installed_resources() -> Result<DesktopResources, DesktopDistributionError>
|
||||
{
|
||||
if let Some(resources_root) = std::env::var_os(DESKTOP_RESOURCES_PATH_ENV_VAR) {
|
||||
return DesktopResources::from_trusted_path(PathBuf::from(resources_root));
|
||||
}
|
||||
discover_installed_distribution()?
|
||||
.map(|distribution| distribution.resources)
|
||||
.ok_or(DesktopDistributionError::NotFound)
|
||||
}
|
||||
|
||||
/// Discovers an installed Desktop app without consulting the launcher resources hint.
|
||||
pub fn discover_installed_distribution()
|
||||
-> Result<Option<InstalledDesktop>, DesktopDistributionError> {
|
||||
platform::discover()
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum ResourceKind {
|
||||
Directory,
|
||||
File,
|
||||
}
|
||||
|
||||
/// Resolves a resource path beneath the trusted Desktop resources root.
|
||||
///
|
||||
/// Rejects empty paths and non-normal components, canonicalizes the joined path to catch symlink
|
||||
/// escapes, requires the result to remain strictly below `root`, and verifies that it has the
|
||||
/// requested file kind.
|
||||
fn contained_path(
|
||||
root: &AbsolutePathBuf,
|
||||
relative_path: &Path,
|
||||
kind: ResourceKind,
|
||||
) -> Result<AbsolutePathBuf, DesktopDistributionError> {
|
||||
if relative_path.as_os_str().is_empty()
|
||||
|| relative_path
|
||||
.components()
|
||||
.any(|component| !matches!(component, Component::Normal(_)))
|
||||
{
|
||||
return Err(containment(
|
||||
"resource paths must contain only normal relative components",
|
||||
));
|
||||
}
|
||||
let path = canonical(root.join(relative_path).as_path(), "resource path")?;
|
||||
if path == *root || !path.as_path().starts_with(root.as_path()) {
|
||||
return Err(containment(
|
||||
"resource must remain strictly below the Desktop resources root",
|
||||
));
|
||||
}
|
||||
let metadata =
|
||||
fs::metadata(path.as_path()).map_err(|source| DesktopDistributionError::Filesystem {
|
||||
stage: "resource metadata",
|
||||
source,
|
||||
})?;
|
||||
let expected_kind = match kind {
|
||||
ResourceKind::Directory => metadata.is_dir(),
|
||||
ResourceKind::File => metadata.is_file(),
|
||||
};
|
||||
if !expected_kind {
|
||||
return Err(containment(match kind {
|
||||
ResourceKind::Directory => "expected a directory",
|
||||
ResourceKind::File => "expected a regular file",
|
||||
}));
|
||||
}
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
fn canonical(
|
||||
path: &Path,
|
||||
stage: &'static str,
|
||||
) -> Result<AbsolutePathBuf, DesktopDistributionError> {
|
||||
AbsolutePathBuf::from_absolute_path_checked(path)
|
||||
.and_then(|path| path.canonicalize())
|
||||
.map_err(|source| DesktopDistributionError::Filesystem { stage, source })
|
||||
}
|
||||
|
||||
fn containment(message: impl Into<String>) -> DesktopDistributionError {
|
||||
DesktopDistributionError::Containment(message.into())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "lib_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,81 @@
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::fs;
|
||||
|
||||
use super::DesktopDistributionError;
|
||||
use super::DesktopResources;
|
||||
#[cfg(unix)]
|
||||
use super::InstalledDesktop;
|
||||
use super::canonical;
|
||||
|
||||
#[test]
|
||||
fn resolves_strictly_contained_files_and_directories() {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let root = temp.path().join("resources");
|
||||
let directory = root.join("plugins/demo");
|
||||
fs::create_dir_all(&directory).expect("create directory");
|
||||
let file = directory.join("hook.json");
|
||||
fs::write(&file, "{}").expect("write file");
|
||||
let resources = DesktopResources::from_trusted_path(root).expect("Desktop resources");
|
||||
|
||||
assert_eq!(
|
||||
resources
|
||||
.contained_directory("plugins/demo")
|
||||
.expect("contained directory"),
|
||||
canonical(&directory, "test directory").expect("canonical directory")
|
||||
);
|
||||
assert_eq!(
|
||||
resources
|
||||
.contained_file("plugins/demo/hook.json")
|
||||
.expect("contained file"),
|
||||
canonical(&file, "test file").expect("canonical file")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_non_normal_and_wrong_kind_paths() {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let root = temp.path().join("resources");
|
||||
fs::create_dir_all(root.join("plugins")).expect("create directory");
|
||||
let resources = DesktopResources::from_trusted_path(root).expect("Desktop resources");
|
||||
|
||||
for path in ["", ".", "../resources/plugins", "/tmp"] {
|
||||
assert!(matches!(
|
||||
resources.contained_directory(path),
|
||||
Err(DesktopDistributionError::Containment(_))
|
||||
));
|
||||
}
|
||||
assert!(matches!(
|
||||
resources.contained_file("plugins"),
|
||||
Err(DesktopDistributionError::Containment(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn rejects_symlink_traversal() {
|
||||
use std::os::unix::fs::symlink;
|
||||
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let root = temp.path().join("resources");
|
||||
let outside = temp.path().join("outside");
|
||||
fs::create_dir_all(&root).expect("create root");
|
||||
fs::create_dir_all(&outside).expect("create outside");
|
||||
fs::write(outside.join("hook.json"), "{}").expect("write file");
|
||||
symlink(&outside, root.join("plugins")).expect("create symlink");
|
||||
let resources = DesktopResources::from_trusted_path(root).expect("Desktop resources");
|
||||
|
||||
assert!(matches!(
|
||||
resources.contained_file("plugins/hook.json"),
|
||||
Err(DesktopDistributionError::Containment(_))
|
||||
));
|
||||
|
||||
let app_root = temp.path().join("Codex.app");
|
||||
let resources_link = app_root.join("Contents/Resources");
|
||||
fs::create_dir_all(resources_link.parent().expect("resources parent"))
|
||||
.expect("create app contents");
|
||||
symlink(&outside, &resources_link).expect("create resources symlink");
|
||||
assert!(matches!(
|
||||
InstalledDesktop::from_paths(app_root, resources_link),
|
||||
Err(DesktopDistributionError::Containment(_))
|
||||
));
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
use crate::DesktopDistributionError;
|
||||
use crate::InstalledDesktop;
|
||||
|
||||
const BUNDLE_IDENTIFIER: &str = "com.openai.codex";
|
||||
const TEAM_IDENTIFIER: &str = "2DC432GLL2";
|
||||
|
||||
/// Asks LaunchServices for the signed stable Codex app, independent of name or location.
|
||||
pub(crate) fn discover() -> Result<Option<InstalledDesktop>, DesktopDistributionError> {
|
||||
let script = format!(r#"POSIX path of (path to application id "{BUNDLE_IDENTIFIER}")"#);
|
||||
let output = Command::new("/usr/bin/osascript")
|
||||
.args(["-e", &script])
|
||||
.output()
|
||||
.map_err(|error| DesktopDistributionError::Discovery(error.to_string()))?;
|
||||
if !output.status.success() {
|
||||
return Ok(None);
|
||||
}
|
||||
let app_root = PathBuf::from(String::from_utf8_lossy(&output.stdout).trim());
|
||||
let resources_root = app_root.join("Contents/Resources");
|
||||
if app_root.extension().is_some_and(|value| value == "app")
|
||||
&& resources_root.is_dir()
|
||||
&& has_openai_signature(&app_root)?
|
||||
{
|
||||
return InstalledDesktop::from_paths(app_root, resources_root).map(Some);
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn has_openai_signature(app_root: &std::path::Path) -> Result<bool, DesktopDistributionError> {
|
||||
let requirement = format!(
|
||||
r#"=identifier "{BUNDLE_IDENTIFIER}" and anchor apple generic and certificate leaf[subject.OU] = "{TEAM_IDENTIFIER}""#
|
||||
);
|
||||
let verification = Command::new("/usr/bin/codesign")
|
||||
.args(["--verify", "--strict", "-R", &requirement])
|
||||
.arg(app_root)
|
||||
.status()
|
||||
.map_err(|error| DesktopDistributionError::Discovery(error.to_string()))?;
|
||||
Ok(verification.success())
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
use crate::DesktopDistributionError;
|
||||
use crate::InstalledDesktop;
|
||||
|
||||
pub(crate) fn discover() -> Result<Option<InstalledDesktop>, DesktopDistributionError> {
|
||||
Ok(None)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
use crate::DesktopDistributionError;
|
||||
use crate::InstalledDesktop;
|
||||
|
||||
const STORE_PUBLISHER_ID: &str = "2p2nqsd0c76g0";
|
||||
|
||||
/// Queries the signed stable MSIX identity and uses the package location as the app root.
|
||||
pub(crate) fn discover() -> Result<Option<InstalledDesktop>, DesktopDistributionError> {
|
||||
let script = format!(
|
||||
r#"
|
||||
$location = Get-AppxPackage -Name 'OpenAI.Codex' -ErrorAction SilentlyContinue |
|
||||
Where-Object {{ $_.PublisherId -eq '{STORE_PUBLISHER_ID}' }} |
|
||||
Select-Object -First 1 -ExpandProperty InstallLocation
|
||||
if ($location) {{ $location }}
|
||||
"#
|
||||
);
|
||||
let output = Command::new("powershell.exe")
|
||||
.arg("-NoProfile")
|
||||
.arg("-NonInteractive")
|
||||
.arg("-Command")
|
||||
.arg(script)
|
||||
.output()
|
||||
.map_err(|error| DesktopDistributionError::Discovery(error.to_string()))?;
|
||||
if !output.status.success() {
|
||||
return Ok(None);
|
||||
}
|
||||
let install_location = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
if install_location.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
let app_root = PathBuf::from(install_location);
|
||||
let resources_root = app_root.join("app/resources");
|
||||
InstalledDesktop::from_paths(app_root, resources_root).map(Some)
|
||||
}
|
||||
Reference in New Issue
Block a user