This commit is contained in:
Javier Soto
2025-11-03 11:33:17 -08:00
parent 84a3cddc76
commit 11028280dc
16 changed files with 722 additions and 39 deletions
+1 -1
View File
@@ -91,7 +91,7 @@ seccompiler = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies]
core-foundation = "0.9"
mac-notification-sys = "0.6.6"
mac-notification-sys = "=0.6.6"
# Build OpenSSL from source for musl builds.
[target.x86_64-unknown-linux-musl.dependencies]
+240 -13
View File
@@ -109,20 +109,254 @@ mod tests {
mod macos {
use super::UserNotification;
use crate::config;
use anyhow::Context;
use anyhow::Result;
use anyhow::anyhow;
use mac_notification_sys::Notification;
use mac_notification_sys::NotificationResponse;
use serde_json;
use std::env;
use std::fs;
use std::io::Write;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use tempfile::NamedTempFile;
use tracing::debug;
use tracing::warn;
const HELPER_ENV: &str = "CODEX_NOTIFIER_APP";
const HELPER_BUNDLE_NAME: &str = "CodexNotifier.app";
const HELPER_EXECUTABLE: &str = "Contents/MacOS/codex-notifier";
const INFO_PLIST_TEMPLATE: &[u8] = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../notifier/Resources/Info.plist"
));
const ICON_BYTES: &[u8] = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../notifier/Resources/Codex.icns"
));
#[derive(Debug)]
pub(crate) struct MacNotifier {
icon_path: Option<PathBuf>,
helper: Option<HelperApp>,
legacy: LegacyNotifier,
}
impl MacNotifier {
pub(crate) fn new() -> Self {
let helper = match HelperApp::discover() {
Ok(app) => {
debug!(
"using Codex helper notifier at {}",
app.bundle_path.display()
);
Some(app)
}
Err(err) => {
debug!("no helper notifier available: {err:?}");
None
}
};
Self {
helper,
legacy: LegacyNotifier::new(),
}
}
pub(crate) fn notify(&self, notification: &UserNotification) {
if env::var("CODEX_SANDBOX").is_ok() {
return;
}
if env::var("CI").is_ok() {
return;
}
if let Some(helper) = &self.helper {
if let Err(err) = helper.notify(notification) {
warn!("failed to send macOS notification via helper: {err:?}");
} else {
return;
}
}
self.legacy.notify(notification);
}
}
#[derive(Debug)]
struct HelperApp {
bundle_path: PathBuf,
}
impl HelperApp {
fn discover() -> Result<Self> {
if let Ok(custom) = env::var(HELPER_ENV) {
let candidate = PathBuf::from(custom);
if Self::is_valid_bundle(&candidate) {
return Ok(Self {
bundle_path: candidate,
});
}
}
for path in Self::candidate_paths()? {
if Self::is_valid_bundle(&path) {
return Ok(Self { bundle_path: path });
}
}
if let Ok(installed) = Self::install_to_codex_home() {
if Self::is_valid_bundle(&installed) {
return Ok(Self {
bundle_path: installed,
});
}
}
Err(anyhow!("helper bundle not found"))
}
fn candidate_paths() -> Result<Vec<PathBuf>> {
let mut candidates = Vec::new();
if let Ok(home) = config::find_codex_home() {
candidates.push(home.join(HELPER_BUNDLE_NAME));
candidates.push(home.join("bin").join(HELPER_BUNDLE_NAME));
}
if let Ok(exe) = env::current_exe() {
if let Some(parent) = exe.parent() {
candidates.push(parent.join(HELPER_BUNDLE_NAME));
if let Some(grand) = parent.parent() {
candidates.push(grand.join(HELPER_BUNDLE_NAME));
}
}
}
Ok(candidates)
}
fn install_to_codex_home() -> Result<PathBuf> {
let mut base = config::find_codex_home()?;
base.push("bin");
fs::create_dir_all(&base)?;
let bundle_path = base.join(HELPER_BUNDLE_NAME);
if !Self::is_valid_bundle(&bundle_path) {
Self::write_bundle(&bundle_path)?;
}
Ok(bundle_path)
}
fn write_bundle(destination: &Path) -> Result<()> {
let contents = destination.join("Contents");
let macos_dir = contents.join("MacOS");
let resources_dir = contents.join("Resources");
fs::create_dir_all(&macos_dir)?;
fs::create_dir_all(&resources_dir)?;
fs::write(contents.join("Info.plist"), INFO_PLIST_TEMPLATE)
.context("failed to write Info.plist")?;
fs::write(resources_dir.join("Codex.icns"), ICON_BYTES)
.context("failed to write Codex.icns")?;
let binary_src = Self::locate_binary().context("codex-notifier binary not found")?;
let binary_dest = macos_dir.join("codex-notifier");
fs::copy(&binary_src, &binary_dest)
.with_context(|| format!("failed to copy notifier binary from {binary_src:?}"))?;
let mut perms = fs::metadata(&binary_dest)?.permissions();
perms.set_mode(0o755);
fs::set_permissions(&binary_dest, perms)?;
Ok(())
}
fn locate_binary() -> Result<PathBuf> {
for candidate in Self::binary_candidate_paths()? {
if candidate.is_file() {
return Ok(candidate);
}
if candidate.is_dir() {
let nested = candidate.join("codex-notifier");
if nested.is_file() {
return Ok(nested);
}
}
}
Err(anyhow!("codex-notifier binary unavailable"))
}
fn binary_candidate_paths() -> Result<Vec<PathBuf>> {
let mut paths = Vec::new();
if let Ok(exe) = env::current_exe() {
if let Some(parent) = exe.parent() {
paths.push(parent.join("codex-notifier"));
paths.push(parent.join("../codex-notifier"));
paths.push(parent.join("../../codex-notifier"));
if let Some(grand) = parent.parent() {
paths.push(grand.join("codex-notifier"));
paths.push(grand.join("release").join("codex-notifier"));
}
}
}
if let Ok(home) = config::find_codex_home() {
paths.push(home.join("bin").join("codex-notifier"));
}
Ok(paths)
}
fn is_valid_bundle(path: &Path) -> bool {
let exec = path.join(HELPER_EXECUTABLE);
exec.is_file()
}
fn notify(&self, notification: &UserNotification) -> Result<()> {
let payload = serde_json::to_vec(notification)?;
let mut temp = NamedTempFile::new().context("failed to create temp payload file")?;
temp.as_file_mut()
.write_all(&payload)
.context("failed to write payload")?;
temp.flush().ok();
let temp_path = temp.into_temp_path();
let payload_path = temp_path
.keep()
.context("failed to persist payload file for helper app")?;
let status = Command::new("open")
.arg("-n")
.arg(&self.bundle_path)
.arg("--args")
.arg("--payload-file")
.arg(&payload_path)
.status()
.with_context(|| {
format!("failed to invoke open for {}", self.bundle_path.display())
})?;
if !status.success() {
let _ = fs::remove_file(&payload_path);
return Err(anyhow!("open exited with status {status:?}"));
}
Ok(())
}
}
#[derive(Debug)]
struct LegacyNotifier {
icon_path: Option<PathBuf>,
}
impl LegacyNotifier {
fn new() -> Self {
if let Err(err) = mac_notification_sys::set_application("com.openai.codex") {
warn!("failed to register bundle id for notifications: {err}");
}
@@ -136,17 +370,7 @@ mod macos {
Self { icon_path }
}
pub(crate) fn notify(&self, notification: &UserNotification) {
if std::env::var("CODEX_SANDBOX").is_ok() {
// Avoid firing real notifications when running inside our sandboxed Seatbelt harness.
return;
}
if std::env::var("CI").is_ok() {
// Skip macOS notifications when running in CI environments.
return;
}
fn notify(&self, notification: &UserNotification) {
let (title, subtitle, message) = match notification {
UserNotification::AgentTurnComplete {
last_assistant_message,
@@ -191,7 +415,7 @@ mod macos {
debug!("Codex notification clicked");
}
Ok(NotificationResponse::None) => {}
Err(err) => warn!("failed to deliver macOS notification: {err}"),
Err(err) => warn!("failed to deliver macOS notification via legacy path: {err}"),
}
}
@@ -213,6 +437,9 @@ mod macos {
if needs_write {
fs::write(&path, ICON_BYTES)?;
let mut perms = fs::metadata(&path)?.permissions();
perms.set_mode(0o644);
fs::set_permissions(&path, perms)?;
}
Ok(path)