From 1904799fd7614c81ae39a3855680141219ae63cb Mon Sep 17 00:00:00 2001 From: martin Date: Thu, 28 May 2026 01:02:09 -0500 Subject: [PATCH 01/17] add codex-primary-runtime for x86_64-linux --- flake.nix | 1 + nix/codex-primary-runtime/default.nix | 36 +++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 nix/codex-primary-runtime/default.nix diff --git a/flake.nix b/flake.nix index 372597e..b53b68f 100644 --- a/flake.nix +++ b/flake.nix @@ -24,6 +24,7 @@ inputs@{ flake-utils, ... }: flake-utils.lib.meld inputs [ ./nix/codex + ./nix/codex-primary-runtime ./default.nix ./nix/fmt.nix ./scripts/fetch_updates diff --git a/nix/codex-primary-runtime/default.nix b/nix/codex-primary-runtime/default.nix new file mode 100644 index 0000000..18d27a1 --- /dev/null +++ b/nix/codex-primary-runtime/default.nix @@ -0,0 +1,36 @@ +{ + flake-utils, + nixpkgs, + ... +}: +flake-utils.lib.eachSystem [ "x86_64-linux" ] ( + system: + let + pkgs = import nixpkgs { inherit system; }; + in + { + packages.codex-primary-runtime = pkgs.stdenvNoCC.mkDerivation { + pname = "codex-primary-runtime"; + version = "26.426.12240"; + + src = pkgs.fetchurl { + url = "https://persistent.oaistatic.com/codex-primary-runtime/26.426.12240/codex-primary-runtime-linux-x64-26.426.12240.tar.xz"; + hash = "sha256-21Yk6276NrZuxvbdBIjO+5ZuSWNoYqq2IJpDNsHKkMQ="; + }; + + sourceRoot = "codex-primary-runtime"; + + dontConfigure = true; + dontBuild = true; + + installPhase = '' + runHook preInstall + + mkdir -p "$out" + cp -R . "$out"/ + + runHook postInstall + ''; + }; + } +) From a64ab10b68b983ab3176ecef43b6c262915b8bb3 Mon Sep 17 00:00:00 2001 From: martin Date: Thu, 28 May 2026 02:27:32 -0500 Subject: [PATCH 02/17] working macOS chrome browser skill ok great, this is 90% of the way there now making sure the manifest installed properly was kinda difficult. also seems like there's layers of caches between a skill updating and the update landing --- default.nix | 61 + flake.nix | 1 + nix/codex-primary-runtime/default.nix | 15 +- src/chrome-extension-host/Cargo.lock | 119 ++ src/chrome-extension-host/Cargo.toml | 13 + src/chrome-extension-host/default.nix | 32 + .../src/bin/codex-chrome-extension-host.rs | 1219 +++++++++++++++++ src/server/main.ts | 5 +- 8 files changed, 1459 insertions(+), 6 deletions(-) create mode 100644 src/chrome-extension-host/Cargo.lock create mode 100644 src/chrome-extension-host/Cargo.toml create mode 100644 src/chrome-extension-host/default.nix create mode 100644 src/chrome-extension-host/src/bin/codex-chrome-extension-host.rs diff --git a/default.nix b/default.nix index 76984d9..1bcd29b 100644 --- a/default.nix +++ b/default.nix @@ -22,6 +22,60 @@ flake-utils.lib.eachSystem systems ( hash = "sha256-zSlRaoUJc4eRFbe08qS/oyqaBbfW2Epjj3hlbEmA6Cw="; }; codex = self.packages.${system}.codex; + isAarch64Darwin = system == "aarch64-darwin"; + isX86_64Linux = system == "x86_64-linux"; + hasCodexWebResources = isAarch64Darwin || isX86_64Linux; + linuxNodeRepl = "${self.packages.${system}.codex-primary-runtime}/dependencies/bin/node_repl"; + codexChromeExtensionHost = self.packages.${system}.codex_chrome_extension_host; + codexWebResources = pkgs.stdenvNoCC.mkDerivation { + pname = "codex-web-resources"; + version = appVersion; + + src = codexZip; + + nativeBuildInputs = [ pkgs.unzip ]; + + dontConfigure = true; + dontBuild = true; + + unpackPhase = '' + runHook preUnpack + + unzip -q "$src" + + runHook postUnpack + ''; + + installPhase = + '' + runHook preInstall + + mkdir -p "$out" + cp -R Codex.app/Contents/Resources/plugins "$out/plugins" + '' + + pkgs.lib.optionalString hasCodexWebResources '' + chromeManifestScript="$out/plugins/openai-bundled/plugins/chrome/scripts/installManifest.mjs" + chromeExtensionHost="${codexChromeExtensionHost}/bin/codex-chrome-extension-host" + substituteInPlace "$chromeManifestScript" \ + --replace-fail 'let t=a(o);' "let t=\"$chromeExtensionHost\";" \ + --replace-fail 'path:a(o)' "path:\"$chromeExtensionHost\"" + '' + + pkgs.lib.optionalString isAarch64Darwin '' + install -m755 Codex.app/Contents/Resources/node "$out/node" + install -m755 Codex.app/Contents/Resources/node_repl "$out/node_repl" + '' + + pkgs.lib.optionalString isX86_64Linux '' + install -m755 ${pkgs.nodejs}/bin/node "$out/node" + install -m755 ${linuxNodeRepl} "$out/node_repl" + '' + + pkgs.lib.optionalString (!hasCodexWebResources) '' + echo "codex-web resources are only packaged for aarch64-darwin and x86_64-linux" >&2 + exit 1 + '' + + '' + runHook postInstall + ''; + }; in { devShells.default = pkgs.mkShell { @@ -111,6 +165,11 @@ flake-utils.lib.eachSystem systems ( patchShebangs scripts ''; + postBuild = '' + substituteInPlace src/server/main.js \ + --replace-fail '@resourcesPath@' '${codexWebResources}' + ''; + preInstall = '' # npm pack always runs the package prepare lifecycle. Nix already ran # the explicit build script above, so remove prepare in the sandbox. @@ -148,6 +207,8 @@ flake-utils.lib.eachSystem systems ( ]; text = builtins.readFile ./scripts/codex_remote_proxy; }; + + codex_web_resources = codexWebResources; }; } ) diff --git a/flake.nix b/flake.nix index b53b68f..ad01077 100644 --- a/flake.nix +++ b/flake.nix @@ -25,6 +25,7 @@ flake-utils.lib.meld inputs [ ./nix/codex ./nix/codex-primary-runtime + ./src/chrome-extension-host ./default.nix ./nix/fmt.nix ./scripts/fetch_updates diff --git a/nix/codex-primary-runtime/default.nix b/nix/codex-primary-runtime/default.nix index 18d27a1..918c47f 100644 --- a/nix/codex-primary-runtime/default.nix +++ b/nix/codex-primary-runtime/default.nix @@ -7,19 +7,30 @@ flake-utils.lib.eachSystem [ "x86_64-linux" ] ( system: let pkgs = import nixpkgs { inherit system; }; + version = "26.426.12240"; + platform = "linux-x64"; in { packages.codex-primary-runtime = pkgs.stdenvNoCC.mkDerivation { pname = "codex-primary-runtime"; - version = "26.426.12240"; + inherit version; src = pkgs.fetchurl { - url = "https://persistent.oaistatic.com/codex-primary-runtime/26.426.12240/codex-primary-runtime-linux-x64-26.426.12240.tar.xz"; + url = "https://persistent.oaistatic.com/codex-primary-runtime/${version}/codex-primary-runtime-${platform}-${version}.tar.xz"; hash = "sha256-21Yk6276NrZuxvbdBIjO+5ZuSWNoYqq2IJpDNsHKkMQ="; }; sourceRoot = "codex-primary-runtime"; + nativeBuildInputs = [ pkgs.autoPatchelfHook ]; + + buildInputs = [ + pkgs.glibc + pkgs.libxcrypt-legacy + pkgs.stdenv.cc.cc.lib + pkgs.zlib + ]; + dontConfigure = true; dontBuild = true; diff --git a/src/chrome-extension-host/Cargo.lock b/src/chrome-extension-host/Cargo.lock new file mode 100644 index 0000000..1e26fcc --- /dev/null +++ b/src/chrome-extension-host/Cargo.lock @@ -0,0 +1,119 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "codex-chrome-extension-host" +version = "0.2.3-linux-alpha1" +dependencies = [ + "anyhow", + "libc", + "serde_json", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/src/chrome-extension-host/Cargo.toml b/src/chrome-extension-host/Cargo.toml new file mode 100644 index 0000000..1274449 --- /dev/null +++ b/src/chrome-extension-host/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "codex-chrome-extension-host" +version = "0.2.3-linux-alpha1" +edition = "2021" + +[[bin]] +name = "codex-chrome-extension-host" +path = "src/bin/codex-chrome-extension-host.rs" + +[dependencies] +anyhow = "=1.0.102" +libc = "=0.2.183" +serde_json = "=1.0.149" diff --git a/src/chrome-extension-host/default.nix b/src/chrome-extension-host/default.nix new file mode 100644 index 0000000..fbb863c --- /dev/null +++ b/src/chrome-extension-host/default.nix @@ -0,0 +1,32 @@ +{ + flake-utils, + nixpkgs, + ... +}: + +flake-utils.lib.eachDefaultSystem ( + system: + let + pkgs = import nixpkgs { inherit system; }; + in + { + packages.codex_chrome_extension_host = pkgs.rustPlatform.buildRustPackage { + pname = "codex-chrome-extension-host"; + version = "0.2.3-linux-alpha1"; + + src = ./.; + + cargoLock = { + lockFile = ./Cargo.lock; + }; + + meta = { + description = "Native messaging host for the Codex Chrome extension"; + homepage = "https://github.com/ilysenko/codex-desktop-linux"; + license = pkgs.lib.licenses.mit; + mainProgram = "codex-chrome-extension-host"; + platforms = pkgs.lib.platforms.unix; + }; + }; + } +) diff --git a/src/chrome-extension-host/src/bin/codex-chrome-extension-host.rs b/src/chrome-extension-host/src/bin/codex-chrome-extension-host.rs new file mode 100644 index 0000000..801071d --- /dev/null +++ b/src/chrome-extension-host/src/bin/codex-chrome-extension-host.rs @@ -0,0 +1,1219 @@ +use anyhow::{bail, Context, Result}; +use serde_json::{json, Value}; +use std::{ + collections::HashMap, + env, fs, + fs::File, + io::{self, BufRead, BufReader, ErrorKind, Read, Seek, SeekFrom, Write}, + net::Shutdown, + os::unix::{ + fs::{MetadataExt, PermissionsExt}, + io::AsRawFd, + net::{UnixListener, UnixStream}, + }, + path::{Path, PathBuf}, + process, + sync::{Arc, Mutex}, + thread, + time::{Duration, Instant, SystemTime, UNIX_EPOCH}, +}; + +const HOST_NAME: &str = "com.openai.codexextension"; +const SOCKET_DIR_ENV: &str = "CODEX_BROWSER_USE_SOCKET_DIR"; +const SESSIONS_DIR_ENV: &str = "CODEX_BROWSER_USE_SESSIONS_DIR"; +const DEFAULT_SOCKET_DIR: &str = "/tmp/codex-browser-use"; +const ROLLOUT_POLL_INTERVAL: Duration = Duration::from_millis(500); +const OBSERVED_TURN_TTL: Duration = Duration::from_secs(6 * 60 * 60); +const ROLLOUT_SEARCH_MAX_DEPTH: usize = 5; + +type SharedState = Arc>; +type SharedClientWriter = Arc>; + +#[derive(Clone)] +struct Client { + writer: SharedClientWriter, +} + +struct PendingChromeRequest { + client_id: usize, + client_request_id: Value, + fallback_extension_info: bool, +} + +#[derive(Clone)] +struct PendingClientRequest { + client_id: usize, + chrome_request_id: Value, +} + +struct PeerCredentials { + pid: Option, + uid: libc::uid_t, +} + +#[derive(Debug, PartialEq, Eq)] +enum ChromeClientRouteError { + NoClients, + MultipleClients, +} + +impl ChromeClientRouteError { + fn message(&self) -> &'static str { + match self { + Self::NoClients => "No Codex browser client is connected", + Self::MultipleClients => { + "Multiple Codex browser clients are connected; Chrome requests require exactly one" + } + } + } +} + +struct HostState { + stdout: Arc>, + rollout_tracker: RolloutTracker, + extension_id: Option, + clients: HashMap, + pending_chrome_requests: HashMap, + pending_client_requests: HashMap, + next_client_id: usize, + next_chrome_id: u64, + next_client_request_id: u64, +} + +impl HostState { + fn new( + stdout: Arc>, + rollout_tracker: RolloutTracker, + extension_id: Option, + ) -> Self { + Self { + stdout, + rollout_tracker, + extension_id, + clients: HashMap::new(), + pending_chrome_requests: HashMap::new(), + pending_client_requests: HashMap::new(), + next_client_id: 1, + next_chrome_id: 1, + next_client_request_id: 1, + } + } + + fn replace_with_client(&mut self, writer: SharedClientWriter) -> (usize, Vec<(usize, Client)>) { + let evicted_clients = self.clients.drain().collect::>(); + if !evicted_clients.is_empty() { + self.pending_chrome_requests.clear(); + self.pending_client_requests.clear(); + } + + let id = self.next_client_id; + self.next_client_id += 1; + self.clients.insert(id, Client { writer }); + (id, evicted_clients) + } + + fn remove_client(&mut self, client_id: usize) { + self.clients.remove(&client_id); + remove_pending_requests_for_client( + &mut self.pending_chrome_requests, + &mut self.pending_client_requests, + client_id, + ); + } + + fn send_chrome(&self, message: &Value) { + let mut stdout = self.stdout.lock().expect("stdout mutex poisoned"); + if let Err(error) = write_frame(&mut *stdout, message) { + log(&format!("native stdout error: {error}")); + process::exit(1); + } + } + + fn send_client(&self, client_id: usize, message: &Value) { + let Some(client) = self.clients.get(&client_id) else { + return; + }; + + let mut writer = client.writer.lock().expect("client writer mutex poisoned"); + if let Err(error) = write_frame(&mut *writer, message) { + log(&format!("client socket write error: {error}")); + } + } + + fn broadcast_clients(&self, message: &Value) { + for client_id in self.clients.keys().copied().collect::>() { + self.send_client(client_id, message); + } + } +} + +#[derive(Clone)] +struct RolloutTracker { + inner: Arc>, + stdout: Arc>, + sessions_root: Option, +} + +struct RolloutTrackerState { + observed: HashMap, +} + +struct ObservedTurn { + session_id: String, + turn_id: String, + path: Option, + offset: u64, + created_at: Instant, +} + +impl RolloutTracker { + fn new(stdout: Arc>) -> Self { + let tracker = Self { + inner: Arc::new(Mutex::new(RolloutTrackerState { + observed: HashMap::new(), + })), + stdout, + sessions_root: sessions_root(), + }; + + let worker = tracker.clone(); + if let Err(error) = thread::Builder::new() + .name("codex-rollout-tracker".to_string()) + .spawn(move || worker.watch_loop()) + { + log(&format!("extension-host: rollout watcher error: {error}")); + } + + tracker + } + + fn observe_request(&self, message: &Value) { + let Some((session_id, turn_id)) = session_turn_from_message(message) else { + return; + }; + + let key = observed_turn_key(&session_id, &turn_id); + let mut state = self.inner.lock().expect("rollout watcher mutex poisoned"); + if state.observed.contains_key(&key) { + return; + } + + let (path, offset) = self + .sessions_root + .as_deref() + .and_then(|root| find_rollout_path(root, &session_id)) + .map(|path| { + let offset = file_len(&path).unwrap_or_default(); + (Some(path), offset) + }) + .unwrap_or((None, 0)); + + state.observed.insert( + key, + ObservedTurn { + session_id, + turn_id, + path, + offset, + created_at: Instant::now(), + }, + ); + } + + fn watch_loop(self) { + loop { + thread::sleep(ROLLOUT_POLL_INTERVAL); + if let Err(error) = self.process_rollouts() { + log(&format!("extension-host: rollout watcher error: {error}")); + } + } + } + + fn process_rollouts(&self) -> Result<()> { + let Some(sessions_root) = self.sessions_root.as_deref() else { + return Ok(()); + }; + + let mut completed = Vec::new(); + let mut expired = Vec::new(); + { + let mut state = self.inner.lock().expect("tracker mutex poisoned"); + for (key, observed) in &mut state.observed { + if observed.created_at.elapsed() >= OBSERVED_TURN_TTL { + expired.push(key.clone()); + continue; + } + + if observed.path.is_none() { + if let Some(path) = find_rollout_path(sessions_root, &observed.session_id) { + observed.offset = 0; + observed.path = Some(path); + } + } + + let Some(path) = observed.path.as_ref() else { + continue; + }; + + let (offset, is_complete) = + drain_rollout_file(path, observed.offset, &observed.turn_id).with_context( + || format!("failed to drain rollout file {}", path.display()), + )?; + observed.offset = offset; + if is_complete { + completed.push(( + key.clone(), + observed.session_id.clone(), + observed.turn_id.clone(), + )); + } + } + + for key in expired { + state.observed.remove(&key); + } + for (key, _, _) in &completed { + state.observed.remove(key); + } + } + + for (_, session_id, turn_id) in completed { + self.emit_turn_ended(&session_id, &turn_id); + } + + Ok(()) + } + + fn emit_turn_ended(&self, session_id: &str, turn_id: &str) { + let message = json!({ + "jsonrpc": "2.0", + "id": format!("native-turn-ended:{session_id}:{turn_id}"), + "method": "turnEnded", + "params": { + "session_id": session_id, + "turn_id": turn_id + } + }); + + let mut stdout = self.stdout.lock().expect("stdout writer mutex poisoned"); + if let Err(error) = write_frame(&mut *stdout, &message) { + log(&format!( + "extension-host: failed to emit turnEnded for session {session_id}: {error}" + )); + } + } +} + +fn main() -> Result<()> { + let socket_dir = socket_dir(); + prepare_socket_dir(&socket_dir)?; + let socket_path = socket_path(&socket_dir); + remove_socket_if_present(&socket_path)?; + + let listener = UnixListener::bind(&socket_path) + .with_context(|| format!("failed to bind {}", socket_path.display()))?; + fs::set_permissions(&socket_path, fs::Permissions::from_mode(0o600)) + .with_context(|| format!("failed to chmod {}", socket_path.display()))?; + + let stdout = Arc::new(Mutex::new(io::stdout())); + let rollout_tracker = RolloutTracker::new(Arc::clone(&stdout)); + let extension_id = extension_id_from_args(); + let state = Arc::new(Mutex::new(HostState::new( + stdout, + rollout_tracker, + extension_id, + ))); + + log(&format!("listening on {}", socket_path.display())); + + { + let state = Arc::clone(&state); + thread::spawn(move || accept_clients(listener, state)); + } + + let result = read_chrome_messages(Arc::clone(&state)); + remove_socket_if_present(&socket_path)?; + result +} + +fn socket_dir() -> PathBuf { + env::var_os(SOCKET_DIR_ENV) + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from(DEFAULT_SOCKET_DIR)) +} + +fn sessions_root() -> Option { + if let Some(path) = env::var_os(SESSIONS_DIR_ENV).map(PathBuf::from) { + return Some(path); + } + + if let Some(path) = env::var_os("CODEX_HOME").map(PathBuf::from) { + return Some(path.join("sessions")); + } + + env::var_os("HOME") + .map(PathBuf::from) + .map(|home| home.join(".codex").join("sessions")) +} + +fn extension_id_from_args() -> Option { + env::args().skip(1).find_map(|arg| { + arg.strip_prefix("chrome-extension://") + .and_then(|value| value.split('/').next()) + .filter(|value| is_extension_id(value)) + .map(ToString::to_string) + }) +} + +fn is_extension_id(value: &str) -> bool { + value.len() == 32 && value.bytes().all(|byte| matches!(byte, b'a'..=b'p')) +} + +fn socket_path(socket_dir: &Path) -> PathBuf { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.as_nanos()) + .unwrap_or_default(); + socket_dir.join(format!("extension-{}-{nonce}.sock", process::id())) +} + +fn prepare_socket_dir(path: &Path) -> Result<()> { + fs::create_dir_all(path).with_context(|| format!("failed to create {}", path.display()))?; + + let metadata = + fs::symlink_metadata(path).with_context(|| format!("failed to stat {}", path.display()))?; + if !metadata.file_type().is_dir() { + bail!( + "unix socket directory path is not a directory: {}", + path.display() + ); + } + + let effective_uid = unsafe { libc::geteuid() }; + if metadata.uid() != effective_uid { + bail!( + "unix socket directory is owned by uid {}, expected {}: {}", + metadata.uid(), + effective_uid, + path.display() + ); + } + + if metadata.permissions().mode() & 0o777 != 0o700 { + fs::set_permissions(path, fs::Permissions::from_mode(0o700)) + .with_context(|| format!("failed to chmod {}", path.display()))?; + } + + Ok(()) +} + +fn remove_socket_if_present(path: &Path) -> Result<()> { + match fs::remove_file(path) { + Ok(()) => Ok(()), + Err(error) if error.kind() == ErrorKind::NotFound => Ok(()), + Err(error) => Err(error).with_context(|| format!("failed to remove {}", path.display())), + } +} + +fn accept_clients(listener: UnixListener, state: SharedState) { + for stream in listener.incoming() { + let stream = match stream { + Ok(stream) => stream, + Err(error) => { + log(&format!("platform accept error: {error}")); + continue; + } + }; + + match authorize_peer(&stream) { + Ok(true) => {} + Ok(false) => continue, + Err(error) => { + log(&format!("peer authorization error: {error}")); + continue; + } + } + + let writer = match stream.try_clone() { + Ok(stream) => Arc::new(Mutex::new(stream)), + Err(error) => { + log(&format!("client socket clone error: {error}")); + continue; + } + }; + + let (client_id, evicted_clients) = { + let mut state = state.lock().expect("host state mutex poisoned"); + state.replace_with_client(writer) + }; + for (evicted_id, evicted_client) in evicted_clients { + log(&format!( + "evicting stale browser client {evicted_id} after a newer client connected" + )); + close_client_socket(&evicted_client); + } + + let state = Arc::clone(&state); + thread::spawn(move || read_client_messages(state, client_id, stream)); + } +} + +fn close_client_socket(client: &Client) { + match client.writer.lock() { + Ok(writer) => { + let _ = writer.shutdown(Shutdown::Both); + } + Err(error) => log(&format!("client socket close lock error: {error}")), + } +} + +fn authorize_peer(stream: &UnixStream) -> Result { + let credentials = peer_credentials(stream)?; + let effective_uid = unsafe { libc::geteuid() }; + + if credentials.uid != effective_uid { + let pid = credentials + .pid + .map(|pid| pid.to_string()) + .unwrap_or_else(|| "unknown".to_string()); + log(&format!( + "rejecting peer pid {} uid {}, expected uid {}", + pid, credentials.uid, effective_uid + )); + return Ok(false); + } + + Ok(true) +} + +#[cfg(target_os = "linux")] +fn peer_credentials(stream: &UnixStream) -> Result { + let mut credentials = libc::ucred { + pid: 0, + uid: 0, + gid: 0, + }; + let mut length = std::mem::size_of::() as libc::socklen_t; + let result = unsafe { + libc::getsockopt( + stream.as_raw_fd(), + libc::SOL_SOCKET, + libc::SO_PEERCRED, + (&mut credentials as *mut libc::ucred).cast(), + &mut length, + ) + }; + + if result != 0 { + return Err(io::Error::last_os_error()).context("failed to read peer credentials"); + } + + Ok(PeerCredentials { + pid: Some(credentials.pid), + uid: credentials.uid, + }) +} + +#[cfg(target_os = "macos")] +fn peer_credentials(stream: &UnixStream) -> Result { + let mut uid: libc::uid_t = 0; + let mut gid: libc::gid_t = 0; + let result = unsafe { libc::getpeereid(stream.as_raw_fd(), &mut uid, &mut gid) }; + + if result != 0 { + return Err(io::Error::last_os_error()).context("failed to read peer credentials"); + } + + Ok(PeerCredentials { pid: None, uid }) +} + +fn read_chrome_messages(state: SharedState) -> Result<()> { + let stdin = io::stdin(); + let mut reader = stdin.lock(); + while let Some(message) = + read_frame(&mut reader).context("extension-host: platform reader error")? + { + handle_chrome_message(&state, message); + } + Ok(()) +} + +fn read_client_messages(state: SharedState, client_id: usize, stream: UnixStream) { + let mut stream = stream; + loop { + match read_frame(&mut stream) { + Ok(Some(message)) => handle_client_message(&state, client_id, message), + Ok(None) => break, + Err(error) => { + log(&format!("client socket read error: {error}")); + break; + } + } + } + + let mut state = state.lock().expect("host state mutex poisoned"); + state.remove_client(client_id); +} + +fn handle_client_message(state: &SharedState, client_id: usize, message: Value) { + { + let state = state.lock().expect("host state mutex poisoned"); + if !state.clients.contains_key(&client_id) { + return; + } + } + + if is_response(&message) { + let Some(id) = message_id_as_str(&message) else { + return; + }; + + let mut state = state.lock().expect("host state mutex poisoned"); + let Some(pending) = state.pending_client_requests.get(id).cloned() else { + return; + }; + if pending.client_id != client_id { + return; + } + state.pending_client_requests.remove(id); + + state.send_chrome(&with_id(message, pending.chrome_request_id)); + return; + } + + if !is_request(&message) { + let state = state.lock().expect("host state mutex poisoned"); + if state.clients.contains_key(&client_id) { + state.send_chrome(&message); + } + return; + } + + { + let tracker = { + let state = state.lock().expect("host state mutex poisoned"); + state.rollout_tracker.clone() + }; + tracker.observe_request(&message); + } + + if message.get("method").and_then(Value::as_str) == Some("ping") { + let Some(id) = message.get("id").cloned() else { + return; + }; + let state = state.lock().expect("host state mutex poisoned"); + state.send_client( + client_id, + &json!({ "jsonrpc": "2.0", "id": id, "result": "pong" }), + ); + return; + } + + let Some(client_request_id) = message.get("id").cloned() else { + return; + }; + let fallback_extension_info = message.get("method").and_then(Value::as_str) == Some("getInfo"); + + let mut state = state.lock().expect("host state mutex poisoned"); + if !state.clients.contains_key(&client_id) { + return; + } + let chrome_id = format!("linux-{}-{}", process::id(), state.next_chrome_id); + state.next_chrome_id += 1; + state.pending_chrome_requests.insert( + chrome_id.clone(), + PendingChromeRequest { + client_id, + client_request_id, + fallback_extension_info, + }, + ); + state.send_chrome(&with_id(message, Value::String(chrome_id))); +} + +fn handle_chrome_message(state: &SharedState, message: Value) { + if is_response(&message) { + let Some(id) = message_id_as_str(&message) else { + return; + }; + + let mut state = state.lock().expect("host state mutex poisoned"); + let Some(pending) = state.pending_chrome_requests.remove(id) else { + return; + }; + + // chrome.runtime.getVersion() is available in Chrome/Chromium 143+. + // Keep forwarding getInfo for browsers that support it, and only + // synthesize discovery metadata for this older-runtime compatibility + // failure. + if pending.fallback_extension_info && is_missing_chrome_runtime_get_version_error(&message) + { + state.send_client( + pending.client_id, + &extension_info_response(pending.client_request_id, state.extension_id.as_deref()), + ); + return; + } + + state.send_client( + pending.client_id, + &with_id(message, pending.client_request_id), + ); + return; + } + + if !is_request(&message) { + let state = state.lock().expect("host state mutex poisoned"); + state.broadcast_clients(&message); + return; + } + + let chrome_request_id = message.get("id").cloned().unwrap_or(Value::Null); + let mut state = state.lock().expect("host state mutex poisoned"); + let client_id = match select_single_client_id(&state.clients) { + Ok(client_id) => client_id, + Err(error) => { + state.send_chrome(&json!({ + "jsonrpc": "2.0", + "id": chrome_request_id, + "error": { + "code": -32000, + "message": error.message() + } + })); + return; + } + }; + + let client_request_id = format!("chrome-{}-{}", process::id(), state.next_client_request_id); + state.next_client_request_id += 1; + state.pending_client_requests.insert( + client_request_id.clone(), + PendingClientRequest { + client_id, + chrome_request_id, + }, + ); + state.send_client( + client_id, + &with_id(message, Value::String(client_request_id)), + ); +} + +fn select_single_client_id( + clients: &HashMap, +) -> std::result::Result { + match clients.len() { + 0 => Err(ChromeClientRouteError::NoClients), + 1 => Ok(*clients.keys().next().expect("one client id")), + _ => Err(ChromeClientRouteError::MultipleClients), + } +} + +fn remove_pending_requests_for_client( + pending_chrome_requests: &mut HashMap, + pending_client_requests: &mut HashMap, + client_id: usize, +) { + pending_chrome_requests.retain(|_, pending| pending.client_id != client_id); + pending_client_requests.retain(|_, pending| pending.client_id != client_id); +} + +fn is_request(message: &Value) -> bool { + message.get("id").is_some() && message.get("method").and_then(Value::as_str).is_some() +} + +fn is_response(message: &Value) -> bool { + message.get("id").is_some() && message.get("method").and_then(Value::as_str).is_none() +} + +fn message_id_as_str(message: &Value) -> Option<&str> { + message.get("id").and_then(Value::as_str) +} + +fn with_id(mut message: Value, id: Value) -> Value { + if let Value::Object(ref mut object) = message { + object.insert("id".to_string(), id); + } + message +} + +fn is_missing_chrome_runtime_get_version_error(message: &Value) -> bool { + message + .get("error") + .and_then(|error| error.get("message")) + .and_then(Value::as_str) + .is_some_and(|message| message.contains("chrome.runtime.getVersion is not a function")) +} + +fn extension_info_response(id: Value, extension_id: Option<&str>) -> Value { + let mut metadata = serde_json::Map::new(); + if let Some(extension_id) = extension_id { + metadata.insert( + "extensionId".to_string(), + Value::String(extension_id.to_string()), + ); + } + + json!({ + "jsonrpc": "2.0", + "id": id, + "result": { + "name": "Chrome", + "version": "unknown", + "type": "extension", + "capabilities": { + "tab": [ + { + "id": "pageAssets", + "description": "List assets already observed in the current page state and bundle selected assets into a temporary local artifact." + } + ] + }, + "metadata": Value::Object(metadata) + } + }) +} + +fn session_turn_from_message(message: &Value) -> Option<(String, String)> { + let params = message.get("params")?; + let session_id = non_empty_string(params.get("session_id")?)?; + let turn_id = non_empty_string(params.get("turn_id")?)?; + Some((session_id.to_string(), turn_id.to_string())) +} + +fn non_empty_string(value: &Value) -> Option<&str> { + let value = value.as_str()?.trim(); + (!value.is_empty()).then_some(value) +} + +fn observed_turn_key(session_id: &str, turn_id: &str) -> String { + format!("{session_id}\n{turn_id}") +} + +fn file_len(path: &Path) -> io::Result { + Ok(fs::metadata(path)?.len()) +} + +fn find_rollout_path(root: &Path, session_id: &str) -> Option { + let mut stack = vec![(root.to_path_buf(), 0_usize)]; + let mut best: Option<(SystemTime, PathBuf)> = None; + + while let Some((dir, depth)) = stack.pop() { + let entries = fs::read_dir(&dir).ok()?; + for entry in entries.flatten() { + let path = entry.path(); + let Ok(file_type) = entry.file_type() else { + continue; + }; + + if file_type.is_dir() { + if depth < ROLLOUT_SEARCH_MAX_DEPTH { + stack.push((path, depth + 1)); + } + continue; + } + + if !file_type.is_file() { + continue; + } + + let file_name = entry.file_name(); + let file_name = file_name.to_string_lossy(); + if !file_name.contains(session_id) + || !(file_name.ends_with(".jsonl") || file_name.ends_with(".json")) + { + continue; + } + + let modified = entry + .metadata() + .and_then(|metadata| metadata.modified()) + .unwrap_or(UNIX_EPOCH); + if best + .as_ref() + .is_none_or(|(best_modified, _)| modified > *best_modified) + { + best = Some((modified, path)); + } + } + } + + best.map(|(_, path)| path) +} + +fn drain_rollout_file(path: &Path, offset: u64, turn_id: &str) -> io::Result<(u64, bool)> { + let mut file = File::open(path)?; + let len = file.metadata()?.len(); + file.seek(SeekFrom::Start(offset.min(len)))?; + + let mut reader = BufReader::new(file); + let mut line = String::new(); + let mut is_complete = false; + + loop { + line.clear(); + if reader.read_line(&mut line)? == 0 { + break; + } + if line_marks_turn_complete(&line, turn_id) { + is_complete = true; + } + } + + Ok((reader.stream_position()?, is_complete)) +} + +fn line_marks_turn_complete(line: &str, turn_id: &str) -> bool { + let Ok(value) = serde_json::from_str::(line) else { + return false; + }; + + let payload = value.get("payload").unwrap_or(&value); + let payload_type = payload.get("type").and_then(Value::as_str); + let payload_turn_id = payload.get("turn_id").and_then(Value::as_str); + if payload_type == Some("task_complete") && payload_turn_id == Some(turn_id) { + return true; + } + + let top_level_type = value.get("type").and_then(Value::as_str); + let kind = value.get("kind").and_then(Value::as_str); + top_level_type == Some("turn") + && matches!(kind, Some("end" | "completed" | "complete")) + && value.get("turn_id").and_then(Value::as_str) == Some(turn_id) +} + +fn read_frame(reader: &mut impl Read) -> io::Result> { + loop { + let mut header = [0_u8; 4]; + match reader.read_exact(&mut header) { + Ok(()) => {} + Err(error) if error.kind() == ErrorKind::UnexpectedEof => return Ok(None), + Err(error) => return Err(error), + } + + let length = u32::from_ne_bytes(header) as usize; + let mut body = vec![0_u8; length]; + reader.read_exact(&mut body)?; + + match serde_json::from_slice(&body) { + Ok(message) => return Ok(Some(message)), + Err(error) => log(&format!("dropping invalid JSON frame: {error}")), + } + } +} + +fn write_frame(writer: &mut impl Write, message: &Value) -> io::Result<()> { + let body = serde_json::to_vec(message).map_err(io::Error::other)?; + if body.len() > u32::MAX as usize { + return Err(io::Error::new( + ErrorKind::InvalidInput, + "message too large for 4-byte length prefix", + )); + } + + writer.write_all(&(body.len() as u32).to_ne_bytes())?; + writer.write_all(&body)?; + writer.flush() +} + +fn log(message: &str) { + let _ = writeln!(io::stderr(), "[{HOST_NAME}] {message}"); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn frame_round_trip_uses_native_length_prefix() { + let message = json!({ "jsonrpc": "2.0", "id": "1", "method": "ping" }); + let mut encoded = Vec::new(); + write_frame(&mut encoded, &message).unwrap(); + + let length = u32::from_ne_bytes(encoded[..4].try_into().unwrap()) as usize; + assert_eq!(length, encoded.len() - 4); + + let mut cursor = io::Cursor::new(encoded); + assert_eq!(read_frame(&mut cursor).unwrap(), Some(message)); + } + + #[test] + fn id_replacement_preserves_other_fields() { + let message = json!({ "jsonrpc": "2.0", "id": 1, "method": "getTabs" }); + assert_eq!( + with_id(message, Value::String("linux-1-1".to_string())), + json!({ "jsonrpc": "2.0", "id": "linux-1-1", "method": "getTabs" }) + ); + } + + #[test] + fn extracts_session_turn_from_browser_request() { + let message = json!({ + "jsonrpc": "2.0", + "id": "request-1", + "method": "getTabs", + "params": { + "session_id": "session-1", + "turn_id": "turn-1" + } + }); + + assert_eq!( + session_turn_from_message(&message), + Some(("session-1".to_string(), "turn-1".to_string())) + ); + } + + #[test] + fn recognizes_task_complete_rollout_line() { + let line = r#"{"timestamp":"2026-05-09T12:00:00Z","type":"event_msg","payload":{"type":"task_complete","turn_id":"turn-1"}}"#; + assert!(line_marks_turn_complete(line, "turn-1")); + assert!(!line_marks_turn_complete(line, "turn-2")); + } + + #[test] + fn finds_nested_rollout_path_by_session_id() { + let root = unique_test_dir("codex-rollout-path"); + let nested = root.join("2026").join("05").join("09"); + fs::create_dir_all(&nested).unwrap(); + let path = nested.join("rollout-2026-05-09T12-00-00-session-1.jsonl"); + fs::write(&path, "{}\n").unwrap(); + + assert_eq!(find_rollout_path(&root, "session-1"), Some(path)); + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn drains_rollout_file_from_offset() { + let root = unique_test_dir("codex-rollout-drain"); + fs::create_dir_all(&root).unwrap(); + let path = root.join("rollout-session-1.jsonl"); + fs::write( + &path, + "{\"type\":\"event_msg\",\"payload\":{\"type\":\"other\"}}\n", + ) + .unwrap(); + let offset = file_len(&path).unwrap(); + + let complete = + r#"{"type":"event_msg","payload":{"type":"task_complete","turn_id":"turn-1"}}"#; + writeln!( + fs::OpenOptions::new().append(true).open(&path).unwrap(), + "ignored\n{complete}" + ) + .unwrap(); + let (new_offset, is_complete) = drain_rollout_file(&path, offset, "turn-1").unwrap(); + + assert!(new_offset >= offset); + assert!(is_complete); + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn late_discovered_rollout_file_scans_existing_content() { + let root = unique_test_dir("codex-rollout-late"); + let nested = root.join("2026").join("05").join("09"); + fs::create_dir_all(&nested).unwrap(); + let path = nested.join("rollout-session-1.jsonl"); + let complete = + r#"{"type":"event_msg","payload":{"type":"task_complete","turn_id":"turn-1"}}"#; + writeln!(File::create(&path).unwrap(), "{complete}").unwrap(); + + let discovered = find_rollout_path(&root, "session-1").unwrap(); + let (_, is_complete) = drain_rollout_file(&discovered, 0, "turn-1").unwrap(); + + assert!(is_complete); + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn rejects_chrome_request_routing_without_exactly_one_client() { + let clients = HashMap::new(); + assert_eq!( + select_single_client_id(&clients), + Err(ChromeClientRouteError::NoClients) + ); + + let mut clients = HashMap::new(); + clients.insert(7, test_client()); + assert_eq!(select_single_client_id(&clients), Ok(7)); + + clients.insert(8, test_client()); + assert_eq!( + select_single_client_id(&clients), + Err(ChromeClientRouteError::MultipleClients) + ); + } + + #[test] + fn replacing_browser_client_evicts_stale_clients_and_pending_requests() { + let mut state = test_host_state(); + + let (first_client_id, evicted_clients) = + state.replace_with_client(test_client().writer.clone()); + assert!(evicted_clients.is_empty()); + assert!(state.clients.contains_key(&first_client_id)); + + state.pending_chrome_requests.insert( + "chrome-request".to_string(), + PendingChromeRequest { + client_id: first_client_id, + client_request_id: json!("client-request-1"), + fallback_extension_info: false, + }, + ); + state.pending_client_requests.insert( + "client-request".to_string(), + PendingClientRequest { + client_id: first_client_id, + chrome_request_id: json!("chrome-request-1"), + }, + ); + + let (second_client_id, evicted_clients) = + state.replace_with_client(test_client().writer.clone()); + + assert_ne!(first_client_id, second_client_id); + assert_eq!(evicted_clients.len(), 1); + assert_eq!(evicted_clients[0].0, first_client_id); + assert!(!state.clients.contains_key(&first_client_id)); + assert!(state.clients.contains_key(&second_client_id)); + assert!(state.pending_chrome_requests.is_empty()); + assert!(state.pending_client_requests.is_empty()); + } + + #[test] + fn evicted_client_requests_are_ignored() { + let state = Arc::new(Mutex::new(test_host_state())); + + handle_client_message( + &state, + 99, + json!({ "jsonrpc": "2.0", "id": 1, "method": "getTabs" }), + ); + + let state = state.lock().unwrap(); + assert!(state.pending_chrome_requests.is_empty()); + assert_eq!(state.next_chrome_id, 1); + } + + #[test] + fn get_info_falls_back_when_runtime_get_version_is_missing() { + let (client_writer, mut client_reader) = UnixStream::pair().unwrap(); + let mut state = test_host_state(); + state.clients.insert( + 1, + Client { + writer: Arc::new(Mutex::new(client_writer)), + }, + ); + state.pending_chrome_requests.insert( + "linux-1-1".to_string(), + PendingChromeRequest { + client_id: 1, + client_request_id: json!("info-1"), + fallback_extension_info: true, + }, + ); + state.extension_id = Some("abcdefghijklmnopabcdefghijklmnop".to_string()); + let state = Arc::new(Mutex::new(state)); + + handle_chrome_message( + &state, + json!({ + "jsonrpc": "2.0", + "id": "linux-1-1", + "error": { + "code": 1, + "message": "chrome.runtime.getVersion is not a function" + } + }), + ); + + let message = read_frame(&mut client_reader).unwrap().unwrap(); + assert_eq!(message["id"], "info-1"); + assert_eq!(message["result"]["type"], "extension"); + assert_eq!(message["result"]["version"], "unknown"); + assert_eq!( + message["result"]["metadata"]["extensionId"], + "abcdefghijklmnopabcdefghijklmnop" + ); + assert!(state.lock().unwrap().pending_chrome_requests.is_empty()); + } + + #[test] + fn disconnect_cleanup_removes_pending_state_for_client() { + let mut pending_chrome = HashMap::from([ + ( + "keep".to_string(), + PendingChromeRequest { + client_id: 1, + client_request_id: json!("chrome-request-1"), + fallback_extension_info: false, + }, + ), + ( + "drop".to_string(), + PendingChromeRequest { + client_id: 2, + client_request_id: json!("chrome-request-2"), + fallback_extension_info: false, + }, + ), + ]); + let mut pending_client = HashMap::from([ + ( + "keep".to_string(), + PendingClientRequest { + client_id: 1, + chrome_request_id: json!("client-request-1"), + }, + ), + ( + "drop".to_string(), + PendingClientRequest { + client_id: 2, + chrome_request_id: json!("client-request-2"), + }, + ), + ]); + + remove_pending_requests_for_client(&mut pending_chrome, &mut pending_client, 2); + + assert!(pending_chrome.contains_key("keep")); + assert!(!pending_chrome.contains_key("drop")); + assert!(pending_client.contains_key("keep")); + assert!(!pending_client.contains_key("drop")); + } + + fn test_client() -> Client { + let (stream, _peer) = UnixStream::pair().unwrap(); + Client { + writer: Arc::new(Mutex::new(stream)), + } + } + + fn test_host_state() -> HostState { + let stdout = Arc::new(Mutex::new(io::stdout())); + HostState::new( + Arc::clone(&stdout), + RolloutTracker { + inner: Arc::new(Mutex::new(RolloutTrackerState { + observed: HashMap::new(), + })), + stdout, + sessions_root: None, + }, + Some("abcdefghijklmnopabcdefghijklmnop".to_string()), + ) + } + + fn unique_test_dir(prefix: &str) -> PathBuf { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + env::temp_dir().join(format!("{prefix}-{}-{nonce}", process::id())) + } +} diff --git a/src/server/main.ts b/src/server/main.ts index a78e8aa..d948e2b 100644 --- a/src/server/main.ts +++ b/src/server/main.ts @@ -243,10 +243,7 @@ function ensureElectronLikeProcessContext(): void { resourcesPath?: string; type?: string; }; - processWithElectronFields.resourcesPath ??= path.resolve( - __dirname, - "../../scratch/asar", - ); + processWithElectronFields.resourcesPath ??= "@resourcesPath@"; processWithElectronFields.type ??= "browser"; } From 81e028839cdc5ca05b3f1743c3fc0a8b92eeb1dd Mon Sep 17 00:00:00 2001 From: martin Date: Thu, 28 May 2026 02:59:41 -0500 Subject: [PATCH 03/17] cleanup messy stuff --- UPGRADING.md | 2 +- default.nix | 70 ++----------------------- flake.nix | 2 + nix/codex-primary-runtime/default.nix | 7 ++- nix/codex-web-resources/default.nix | 73 +++++++++++++++++++++++++++ nix/codex-zip/default.nix | 30 +++++++++++ 6 files changed, 115 insertions(+), 69 deletions(-) create mode 100644 nix/codex-web-resources/default.nix create mode 100644 nix/codex-zip/default.nix diff --git a/UPGRADING.md b/UPGRADING.md index a622475..6bf0a17 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -22,7 +22,7 @@ the context the patches were being applied in. there are a few places to update next. -1. `appVersion` in default.nix and `hash` in `codexZip`. +1. `version` and `hash` in `nix/codex-zip/default.nix`. 2. `APP_VERSION` in ./scripts/prepare then temporarily comment out the patch lines in ./scripts/prepare_asar and run diff --git a/default.nix b/default.nix index 1bcd29b..e7a4544 100644 --- a/default.nix +++ b/default.nix @@ -16,73 +16,13 @@ flake-utils.lib.eachSystem systems ( system: let pkgs = import nixpkgs { inherit system; }; - appVersion = "26.513.20950"; - codexZip = pkgs.fetchurl { - url = "https://persistent.oaistatic.com/codex-app-prod/Codex-darwin-arm64-${appVersion}.zip"; - hash = "sha256-zSlRaoUJc4eRFbe08qS/oyqaBbfW2Epjj3hlbEmA6Cw="; - }; - codex = self.packages.${system}.codex; - isAarch64Darwin = system == "aarch64-darwin"; - isX86_64Linux = system == "x86_64-linux"; - hasCodexWebResources = isAarch64Darwin || isX86_64Linux; - linuxNodeRepl = "${self.packages.${system}.codex-primary-runtime}/dependencies/bin/node_repl"; - codexChromeExtensionHost = self.packages.${system}.codex_chrome_extension_host; - codexWebResources = pkgs.stdenvNoCC.mkDerivation { - pname = "codex-web-resources"; - version = appVersion; - - src = codexZip; - - nativeBuildInputs = [ pkgs.unzip ]; - - dontConfigure = true; - dontBuild = true; - - unpackPhase = '' - runHook preUnpack - - unzip -q "$src" - - runHook postUnpack - ''; - - installPhase = - '' - runHook preInstall - - mkdir -p "$out" - cp -R Codex.app/Contents/Resources/plugins "$out/plugins" - '' - + pkgs.lib.optionalString hasCodexWebResources '' - chromeManifestScript="$out/plugins/openai-bundled/plugins/chrome/scripts/installManifest.mjs" - chromeExtensionHost="${codexChromeExtensionHost}/bin/codex-chrome-extension-host" - substituteInPlace "$chromeManifestScript" \ - --replace-fail 'let t=a(o);' "let t=\"$chromeExtensionHost\";" \ - --replace-fail 'path:a(o)' "path:\"$chromeExtensionHost\"" - '' - + pkgs.lib.optionalString isAarch64Darwin '' - install -m755 Codex.app/Contents/Resources/node "$out/node" - install -m755 Codex.app/Contents/Resources/node_repl "$out/node_repl" - '' - + pkgs.lib.optionalString isX86_64Linux '' - install -m755 ${pkgs.nodejs}/bin/node "$out/node" - install -m755 ${linuxNodeRepl} "$out/node_repl" - '' - + pkgs.lib.optionalString (!hasCodexWebResources) '' - echo "codex-web resources are only packaged for aarch64-darwin and x86_64-linux" >&2 - exit 1 - '' - + '' - runHook postInstall - ''; - }; in { devShells.default = pkgs.mkShell { - HOSTED_CODEX_APP_ZIP = codexZip; + HOSTED_CODEX_APP_ZIP = self.packages.${system}.codexZip; packages = [ - codex + self.packages.${system}.codex pkgs.nodejs pkgs.unzip pkgs.patch @@ -143,7 +83,7 @@ flake-utils.lib.eachSystem systems ( in { default = pkgs.buildNpmPackage { - HOSTED_CODEX_APP_ZIP = codexZip; + HOSTED_CODEX_APP_ZIP = self.packages.${system}.codexZip; pname = "codex-web"; version = "1.0.0"; @@ -167,7 +107,7 @@ flake-utils.lib.eachSystem systems ( postBuild = '' substituteInPlace src/server/main.js \ - --replace-fail '@resourcesPath@' '${codexWebResources}' + --replace-fail '@resourcesPath@' '${self.packages.${system}.codexWebResources}' ''; preInstall = '' @@ -207,8 +147,6 @@ flake-utils.lib.eachSystem systems ( ]; text = builtins.readFile ./scripts/codex_remote_proxy; }; - - codex_web_resources = codexWebResources; }; } ) diff --git a/flake.nix b/flake.nix index ad01077..8e63292 100644 --- a/flake.nix +++ b/flake.nix @@ -24,7 +24,9 @@ inputs@{ flake-utils, ... }: flake-utils.lib.meld inputs [ ./nix/codex + ./nix/codex-zip ./nix/codex-primary-runtime + ./nix/codex-web-resources ./src/chrome-extension-host ./default.nix ./nix/fmt.nix diff --git a/nix/codex-primary-runtime/default.nix b/nix/codex-primary-runtime/default.nix index 918c47f..c6257d4 100644 --- a/nix/codex-primary-runtime/default.nix +++ b/nix/codex-primary-runtime/default.nix @@ -8,15 +8,18 @@ flake-utils.lib.eachSystem [ "x86_64-linux" ] ( let pkgs = import nixpkgs { inherit system; }; version = "26.426.12240"; - platform = "linux-x64"; in { packages.codex-primary-runtime = pkgs.stdenvNoCC.mkDerivation { pname = "codex-primary-runtime"; inherit version; + # this one random version of codex-primary-runtime has a linux build with + # node_repl mcp. considered using the macOS binary and emulating with + # darling but ran into numerous issues with darling's isolation + # requirements src = pkgs.fetchurl { - url = "https://persistent.oaistatic.com/codex-primary-runtime/${version}/codex-primary-runtime-${platform}-${version}.tar.xz"; + url = "https://persistent.oaistatic.com/codex-primary-runtime/${version}/codex-primary-runtime-linux-x64-${version}.tar.xz"; hash = "sha256-21Yk6276NrZuxvbdBIjO+5ZuSWNoYqq2IJpDNsHKkMQ="; }; diff --git a/nix/codex-web-resources/default.nix b/nix/codex-web-resources/default.nix new file mode 100644 index 0000000..fba897f --- /dev/null +++ b/nix/codex-web-resources/default.nix @@ -0,0 +1,73 @@ +{ + self, + flake-utils, + nixpkgs, + ... +}: +let + systems = [ + "aarch64-darwin" + "x86_64-darwin" + "aarch64-linux" + "x86_64-linux" + ]; +in +flake-utils.lib.eachSystem systems ( + system: + let + pkgs = import nixpkgs { inherit system; }; + in + { + packages.codexWebResources = pkgs.stdenvNoCC.mkDerivation { + pname = "codex-web-resources"; + version = self.packages.${system}.codexZip.version; + + src = self.packages.${system}.codexZip; + + nativeBuildInputs = [ pkgs.unzip ]; + + dontConfigure = true; + dontBuild = true; + + unpackPhase = '' + runHook preUnpack + + unzip -q "$src" + + runHook postUnpack + ''; + + installPhase = '' + runHook preInstall + + mkdir -p "$out" + cp -R Codex.app/Contents/Resources/plugins "$out/plugins" + + chromeManifestScript="$out/plugins/openai-bundled/plugins/chrome/scripts/installManifest.mjs" + chromeExtensionHost="${ + self.packages.${system}.codex_chrome_extension_host + }/bin/codex-chrome-extension-host" + substituteInPlace "$chromeManifestScript" \ + --replace-fail 'let t=a(o);' "let t=\"$chromeExtensionHost\";" \ + --replace-fail 'path:a(o)' "path:\"$chromeExtensionHost\"" + '' + + pkgs.lib.optionalString (system == "aarch64-darwin") '' + install -m755 Codex.app/Contents/Resources/node "$out/node" + install -m755 Codex.app/Contents/Resources/node_repl "$out/node_repl" + '' + + pkgs.lib.optionalString (system == "x86_64-linux") '' + install -m755 ${pkgs.nodejs}/bin/node "$out/node" + install -m755 ${ + self.packages.${system}.codex-primary-runtime + }/dependencies/bin/node_repl "$out/node_repl" + '' + + pkgs.lib.optionalString (system != "aarch64-darwin" && system != "x86_64-linux") '' + echo "codex-web resources are only packaged for aarch64-darwin and x86_64-linux" >&2 + exit 1 + '' + + '' + runHook postInstall + ''; + }; + } +) diff --git a/nix/codex-zip/default.nix b/nix/codex-zip/default.nix new file mode 100644 index 0000000..d637f9e --- /dev/null +++ b/nix/codex-zip/default.nix @@ -0,0 +1,30 @@ +{ + flake-utils, + nixpkgs, + ... +}: +let + systems = [ + "aarch64-darwin" + "x86_64-darwin" + "aarch64-linux" + "x86_64-linux" + ]; +in +flake-utils.lib.eachSystem systems ( + system: + let + pkgs = import nixpkgs { inherit system; }; + version = "26.513.20950"; + in + { + packages.codexZip = pkgs.fetchurl { + name = "codex-darwin-arm64-${version}.zip"; + url = "https://persistent.oaistatic.com/codex-app-prod/Codex-darwin-arm64-${version}.zip"; + hash = "sha256-zSlRaoUJc4eRFbe08qS/oyqaBbfW2Epjj3hlbEmA6Cw="; + passthru = { + inherit version; + }; + }; + } +) From dc0be365b6bc50a1d90b3fc6649b6695961e6a7c Mon Sep 17 00:00:00 2001 From: martin Date: Thu, 28 May 2026 03:11:04 -0500 Subject: [PATCH 04/17] naming --- default.nix | 2 +- flake.nix | 2 +- nix/{codex-web-resources => codex-resources}/default.nix | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) rename nix/{codex-web-resources => codex-resources}/default.nix (90%) diff --git a/default.nix b/default.nix index e7a4544..4f13c5b 100644 --- a/default.nix +++ b/default.nix @@ -107,7 +107,7 @@ flake-utils.lib.eachSystem systems ( postBuild = '' substituteInPlace src/server/main.js \ - --replace-fail '@resourcesPath@' '${self.packages.${system}.codexWebResources}' + --replace-fail '@resourcesPath@' '${self.packages.${system}.codex_resources}' ''; preInstall = '' diff --git a/flake.nix b/flake.nix index 8e63292..deff901 100644 --- a/flake.nix +++ b/flake.nix @@ -26,7 +26,7 @@ ./nix/codex ./nix/codex-zip ./nix/codex-primary-runtime - ./nix/codex-web-resources + ./nix/codex-resources ./src/chrome-extension-host ./default.nix ./nix/fmt.nix diff --git a/nix/codex-web-resources/default.nix b/nix/codex-resources/default.nix similarity index 90% rename from nix/codex-web-resources/default.nix rename to nix/codex-resources/default.nix index fba897f..f4853f5 100644 --- a/nix/codex-web-resources/default.nix +++ b/nix/codex-resources/default.nix @@ -18,8 +18,8 @@ flake-utils.lib.eachSystem systems ( pkgs = import nixpkgs { inherit system; }; in { - packages.codexWebResources = pkgs.stdenvNoCC.mkDerivation { - pname = "codex-web-resources"; + packages.codex_resources = pkgs.stdenvNoCC.mkDerivation { + pname = "codex-resources"; version = self.packages.${system}.codexZip.version; src = self.packages.${system}.codexZip; @@ -62,7 +62,7 @@ flake-utils.lib.eachSystem systems ( }/dependencies/bin/node_repl "$out/node_repl" '' + pkgs.lib.optionalString (system != "aarch64-darwin" && system != "x86_64-linux") '' - echo "codex-web resources are only packaged for aarch64-darwin and x86_64-linux" >&2 + echo "codex resources are only packaged for aarch64-darwin and x86_64-linux" >&2 exit 1 '' + '' From 316f6551f069be80e2f70fa69465d62367ab2ab3 Mon Sep 17 00:00:00 2001 From: martin Date: Thu, 28 May 2026 04:05:36 -0500 Subject: [PATCH 05/17] attempt brave setup for linux hopefully this works? --- nix/codex-resources/default.nix | 112 +++++++++++++++++- .../patches/chrome-linux-brave-skill.patch | 38 ++++++ 2 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 nix/codex-resources/patches/chrome-linux-brave-skill.patch diff --git a/nix/codex-resources/default.nix b/nix/codex-resources/default.nix index f4853f5..ef74d38 100644 --- a/nix/codex-resources/default.nix +++ b/nix/codex-resources/default.nix @@ -16,6 +16,107 @@ flake-utils.lib.eachSystem systems ( system: let pkgs = import nixpkgs { inherit system; }; + linuxOpenChromeWindow = + let + linuxUserDataTemplate = + let + chromeExtensionId = "hehggadaopoacecdllhhajmbjkdcmajg"; + chromeNativeHostName = "com.openai.codexextension"; + chromeExtensionUpdateUrl = "https://clients2.google.com/service/update2/crx"; + in + pkgs.linkFarm "codex-brave-user-data-template" [ + { + name = "NativeMessagingHosts/${chromeNativeHostName}.json"; + path = pkgs.writeText "codex-chrome-native-host-manifest.json" ( + builtins.toJSON { + name = chromeNativeHostName; + description = "Codex chrome native messaging host"; + type = "stdio"; + path = "${self.packages.${system}.codex_chrome_extension_host}/bin/codex-chrome-extension-host"; + allowed_origins = [ "chrome-extension://${chromeExtensionId}/" ]; + } + ); + } + { + name = "External Extensions/${chromeExtensionId}.json"; + path = pkgs.writeText "codex-chrome-extension.json" ( + builtins.toJSON { + external_update_url = chromeExtensionUpdateUrl; + } + ); + } + { + name = "policies/managed/codex.json"; + path = pkgs.writeText "codex-chrome-policy.json" ( + builtins.toJSON { + ExtensionInstallForcelist = [ "${chromeExtensionId};${chromeExtensionUpdateUrl}" ]; + AudioCaptureAllowed = false; + VideoCaptureAllowed = false; + DefaultClipboardSetting = 2; + DefaultWebUsbGuardSetting = 2; + DefaultSerialGuardSetting = 2; + } + ); + } + { + name = "Codex/Preferences"; + path = pkgs.writeText "codex-chrome-preferences.json" ( + builtins.toJSON { + profile = { + name = "Codex"; + }; + extensions = { + settings = { + "${chromeExtensionId}" = { + external_update_url = chromeExtensionUpdateUrl; + }; + }; + }; + } + ); + } + ]; + in + pkgs.writeShellScriptBin "codex-open-chrome-window" '' + set -euo pipefail + + if [[ "$#" -gt 0 ]]; then + echo "Usage: scripts/open-chrome-window.js" >&2 + exit 2 + fi + + profile_root="$(mktemp -d -t codex-brave-profile.XXXXXX)" + + home_dir="$profile_root/home" + xdg_config_home="$profile_root/xdg-config" + xdg_cache_home="$profile_root/xdg-cache" + user_data_dir="$profile_root/user-data" + profile_name="Codex" + + mkdir -p \ + "$home_dir" \ + "$xdg_config_home" \ + "$xdg_cache_home" \ + "$user_data_dir" + + cp -RL --no-preserve=mode,ownership,timestamps ${linuxUserDataTemplate}/. "$user_data_dir" + chmod -R u+w "$user_data_dir" + + log_file="$profile_root/brave.log" + HOME="$home_dir" XDG_CONFIG_HOME="$xdg_config_home" XDG_CACHE_HOME="$xdg_cache_home" \ + ${pkgs.xvfb-run}/bin/xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" \ + ${pkgs.brave}/bin/brave \ + --user-data-dir="$user_data_dir" \ + --profile-directory="$profile_name" \ + --no-first-run \ + --no-default-browser-check \ + --disable-dev-shm-usage \ + --new-window about:blank \ + > "$log_file" 2>&1 & + + echo "$!" > "$profile_root/xvfb-run.pid" + echo "Started Brave with profile root: $profile_root" + ''; in { packages.codex_resources = pkgs.stdenvNoCC.mkDerivation { @@ -24,7 +125,10 @@ flake-utils.lib.eachSystem systems ( src = self.packages.${system}.codexZip; - nativeBuildInputs = [ pkgs.unzip ]; + nativeBuildInputs = [ + pkgs.patch + pkgs.unzip + ]; dontConfigure = true; dontBuild = true; @@ -51,6 +155,12 @@ flake-utils.lib.eachSystem systems ( --replace-fail 'let t=a(o);' "let t=\"$chromeExtensionHost\";" \ --replace-fail 'path:a(o)' "path:\"$chromeExtensionHost\"" '' + + pkgs.lib.optionalString (system == "x86_64-linux") '' + chromePluginRoot="$out/plugins/openai-bundled/plugins/chrome" + patch --batch --forward --strip 1 --directory "$chromePluginRoot" < ${./patches/chrome-linux-brave-skill.patch} + rm "$chromePluginRoot/scripts/open-chrome-window.js" + ln -s ${linuxOpenChromeWindow}/bin/codex-open-chrome-window "$chromePluginRoot/scripts/open-chrome-window.js" + '' + pkgs.lib.optionalString (system == "aarch64-darwin") '' install -m755 Codex.app/Contents/Resources/node "$out/node" install -m755 Codex.app/Contents/Resources/node_repl "$out/node_repl" diff --git a/nix/codex-resources/patches/chrome-linux-brave-skill.patch b/nix/codex-resources/patches/chrome-linux-brave-skill.patch new file mode 100644 index 0000000..7f40336 --- /dev/null +++ b/nix/codex-resources/patches/chrome-linux-brave-skill.patch @@ -0,0 +1,38 @@ +--- a/skills/chrome/SKILL.md ++++ b/skills/chrome/SKILL.md +@@ -46,6 +46,14 @@ + Keep the first response short and non-technical unless the user asks for more information. + + If Chrome is not running then ALWAYS ask the User if they would like to launch Chrome. ALWAYS wait for a user response before taking action. ++ ++If the User agrees, run: ++ ++``` ++scripts/open-chrome-window.js ++``` ++ ++Then wait 2 seconds and retry the browser-client setup once. + + + ### 3. The native host manifest is not installed, or is invalid +@@ -141,19 +149,13 @@ + + ### open-chrome-window.js + +-This script opens `about:blank` in a Google Chrome window for the same selected Chrome profile used by `check-extension-installed.js`. Use it only after the User gives permission. ++This script opens a browser window for the selected Chrome automation profile. On Linux, the packaged script starts Brave from Nix under Xvfb with a fresh temporary profile configured for the Codex Chrome Extension and native host bridge. Use it only after the User gives permission. + + From the plugin root, use `node_repl` to run: + + ``` + scripts/open-chrome-window.js +-``` +- +-Use dry-run JSON output when another tool or script needs to verify the selected launch command without opening Chrome: +- + ``` +-scripts/open-chrome-window.js --dry-run --json +-``` + + ### check-extension-installed.js + From 802c8d44c3ce60beb1399795244be05daf4e6399 Mon Sep 17 00:00:00 2001 From: martin Date: Thu, 28 May 2026 04:55:06 -0500 Subject: [PATCH 06/17] Prefer resourcesPath for browser-use runtimes --- patches/browser-use-resources-path-runtime.patch | 14 ++++++++++++++ scripts/prepare_asar | 1 + 2 files changed, 15 insertions(+) create mode 100644 patches/browser-use-resources-path-runtime.patch diff --git a/patches/browser-use-resources-path-runtime.patch b/patches/browser-use-resources-path-runtime.patch new file mode 100644 index 0000000..e670114 --- /dev/null +++ b/patches/browser-use-resources-path-runtime.patch @@ -0,0 +1,14 @@ +--- a/.vite/build/main-kSlb32Yb.js ++++ b/.vite/build/main-kSlb32Yb.js +@@ -968,7 +968,10 @@ + resolveBundledPath: a, + resourcesPath: o, + }) { +- if (t || n === `win32`) return a(o); ++ if (o != null) { ++ let s = a(o); ++ if (t || n === `win32` || Gt(s)) return s; ++ } else if (t || n === `win32`) return a(o); + let s = i.default.join(r, ...e); + return Gt(s) ? s : null; + } diff --git a/scripts/prepare_asar b/scripts/prepare_asar index b1782aa..5fed81b 100755 --- a/scripts/prepare_asar +++ b/scripts/prepare_asar @@ -35,6 +35,7 @@ patch --batch --forward --strip 1 --directory scratch/asar < patches/webview-ele patch --batch --forward --strip 1 --directory scratch/asar < patches/webview-prosemirror-inputmode.patch patch --batch --forward --strip 1 --directory scratch/asar < patches/webview-use-atfs-for-local-files.patch patch --batch --forward --strip 1 --directory scratch/asar < patches/webview-prompt-search-param.patch +patch --batch --forward --strip 1 --directory scratch/asar < patches/browser-use-resources-path-runtime.patch patch --batch --forward --strip 1 --directory scratch/asar < patches/sentry-disable-shell.patch patch --batch --forward --strip 1 --directory scratch/asar < patches/sentry-disable-webview.patch rm -rf scratch/asar/node_modules/better-sqlite3 From b640c03d41bb207d405b1dfaf0a4f9a73c161f73 Mon Sep 17 00:00:00 2001 From: martin Date: Thu, 28 May 2026 05:14:58 -0500 Subject: [PATCH 07/17] Fix Brave native messaging manifest path --- nix/codex-resources/default.nix | 117 ++++++++++++++++---------------- 1 file changed, 58 insertions(+), 59 deletions(-) diff --git a/nix/codex-resources/default.nix b/nix/codex-resources/default.nix index ef74d38..d1693d6 100644 --- a/nix/codex-resources/default.nix +++ b/nix/codex-resources/default.nix @@ -18,64 +18,62 @@ flake-utils.lib.eachSystem systems ( pkgs = import nixpkgs { inherit system; }; linuxOpenChromeWindow = let - linuxUserDataTemplate = - let - chromeExtensionId = "hehggadaopoacecdllhhajmbjkdcmajg"; - chromeNativeHostName = "com.openai.codexextension"; - chromeExtensionUpdateUrl = "https://clients2.google.com/service/update2/crx"; - in - pkgs.linkFarm "codex-brave-user-data-template" [ - { - name = "NativeMessagingHosts/${chromeNativeHostName}.json"; - path = pkgs.writeText "codex-chrome-native-host-manifest.json" ( - builtins.toJSON { - name = chromeNativeHostName; - description = "Codex chrome native messaging host"; - type = "stdio"; - path = "${self.packages.${system}.codex_chrome_extension_host}/bin/codex-chrome-extension-host"; - allowed_origins = [ "chrome-extension://${chromeExtensionId}/" ]; - } - ); - } - { - name = "External Extensions/${chromeExtensionId}.json"; - path = pkgs.writeText "codex-chrome-extension.json" ( - builtins.toJSON { - external_update_url = chromeExtensionUpdateUrl; - } - ); - } - { - name = "policies/managed/codex.json"; - path = pkgs.writeText "codex-chrome-policy.json" ( - builtins.toJSON { - ExtensionInstallForcelist = [ "${chromeExtensionId};${chromeExtensionUpdateUrl}" ]; - AudioCaptureAllowed = false; - VideoCaptureAllowed = false; - DefaultClipboardSetting = 2; - DefaultWebUsbGuardSetting = 2; - DefaultSerialGuardSetting = 2; - } - ); - } - { - name = "Codex/Preferences"; - path = pkgs.writeText "codex-chrome-preferences.json" ( - builtins.toJSON { - profile = { - name = "Codex"; - }; - extensions = { - settings = { - "${chromeExtensionId}" = { - external_update_url = chromeExtensionUpdateUrl; - }; + chromeExtensionId = "hehggadaopoacecdllhhajmbjkdcmajg"; + chromeNativeHostName = "com.openai.codexextension"; + chromeExtensionUpdateUrl = "https://clients2.google.com/service/update2/crx"; + chromeNativeHostManifest = pkgs.writeText "codex-chrome-native-host-manifest.json" ( + builtins.toJSON { + name = chromeNativeHostName; + description = "Codex chrome native messaging host"; + type = "stdio"; + path = "${self.packages.${system}.codex_chrome_extension_host}/bin/codex-chrome-extension-host"; + allowed_origins = [ "chrome-extension://${chromeExtensionId}/" ]; + } + ); + linuxProfileRootTemplate = pkgs.linkFarm "codex-brave-profile-root-template" [ + { + name = "xdg-config/BraveSoftware/Brave-Browser/NativeMessagingHosts/${chromeNativeHostName}.json"; + path = chromeNativeHostManifest; + } + { + name = "user-data/External Extensions/${chromeExtensionId}.json"; + path = pkgs.writeText "codex-chrome-extension.json" ( + builtins.toJSON { + external_update_url = chromeExtensionUpdateUrl; + } + ); + } + { + name = "user-data/policies/managed/codex.json"; + path = pkgs.writeText "codex-chrome-policy.json" ( + builtins.toJSON { + ExtensionInstallForcelist = [ "${chromeExtensionId};${chromeExtensionUpdateUrl}" ]; + AudioCaptureAllowed = false; + VideoCaptureAllowed = false; + DefaultClipboardSetting = 2; + DefaultWebUsbGuardSetting = 2; + DefaultSerialGuardSetting = 2; + } + ); + } + { + name = "user-data/Codex/Preferences"; + path = pkgs.writeText "codex-chrome-preferences.json" ( + builtins.toJSON { + profile = { + name = "Codex"; + }; + extensions = { + settings = { + "${chromeExtensionId}" = { + external_update_url = chromeExtensionUpdateUrl; }; }; - } - ); - } - ]; + }; + } + ); + } + ]; in pkgs.writeShellScriptBin "codex-open-chrome-window" '' set -euo pipefail @@ -93,14 +91,15 @@ flake-utils.lib.eachSystem systems ( user_data_dir="$profile_root/user-data" profile_name="Codex" + cp -RL --no-preserve=mode,ownership,timestamps ${linuxProfileRootTemplate}/. "$profile_root" + mkdir -p \ "$home_dir" \ - "$xdg_config_home" \ "$xdg_cache_home" \ + "$xdg_config_home" \ "$user_data_dir" - cp -RL --no-preserve=mode,ownership,timestamps ${linuxUserDataTemplate}/. "$user_data_dir" - chmod -R u+w "$user_data_dir" + chmod -R u+w "$profile_root" log_file="$profile_root/brave.log" HOME="$home_dir" XDG_CONFIG_HOME="$xdg_config_home" XDG_CACHE_HOME="$xdg_cache_home" \ From 7fab619d884c9659c1c3bdd7ba5b60099b2dbb74 Mon Sep 17 00:00:00 2001 From: martin Date: Thu, 28 May 2026 05:28:22 -0500 Subject: [PATCH 08/17] committing --- nix/codex-resources/default.nix | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nix/codex-resources/default.nix b/nix/codex-resources/default.nix index d1693d6..b8d06f3 100644 --- a/nix/codex-resources/default.nix +++ b/nix/codex-resources/default.nix @@ -35,6 +35,10 @@ flake-utils.lib.eachSystem systems ( name = "xdg-config/BraveSoftware/Brave-Browser/NativeMessagingHosts/${chromeNativeHostName}.json"; path = chromeNativeHostManifest; } + { + name = "home/.config/BraveSoftware/Brave-Browser/NativeMessagingHosts/${chromeNativeHostName}.json"; + path = chromeNativeHostManifest; + } { name = "user-data/External Extensions/${chromeExtensionId}.json"; path = pkgs.writeText "codex-chrome-extension.json" ( From 13bf34ffb0f774d8fbdabcb701bd18f32b7b569d Mon Sep 17 00:00:00 2001 From: martin Date: Thu, 28 May 2026 05:52:57 -0500 Subject: [PATCH 09/17] remove extra --- nix/codex-resources/default.nix | 4 ---- 1 file changed, 4 deletions(-) diff --git a/nix/codex-resources/default.nix b/nix/codex-resources/default.nix index b8d06f3..d1693d6 100644 --- a/nix/codex-resources/default.nix +++ b/nix/codex-resources/default.nix @@ -35,10 +35,6 @@ flake-utils.lib.eachSystem systems ( name = "xdg-config/BraveSoftware/Brave-Browser/NativeMessagingHosts/${chromeNativeHostName}.json"; path = chromeNativeHostManifest; } - { - name = "home/.config/BraveSoftware/Brave-Browser/NativeMessagingHosts/${chromeNativeHostName}.json"; - path = chromeNativeHostManifest; - } { name = "user-data/External Extensions/${chromeExtensionId}.json"; path = pkgs.writeText "codex-chrome-extension.json" ( From 43d2d75d7df084f11202b07e2a77ba2dc4c7e302 Mon Sep 17 00:00:00 2001 From: martin Date: Thu, 28 May 2026 06:09:36 -0500 Subject: [PATCH 10/17] fix hup --- nix/codex-resources/default.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/nix/codex-resources/default.nix b/nix/codex-resources/default.nix index d1693d6..cb70200 100644 --- a/nix/codex-resources/default.nix +++ b/nix/codex-resources/default.nix @@ -103,6 +103,7 @@ flake-utils.lib.eachSystem systems ( log_file="$profile_root/brave.log" HOME="$home_dir" XDG_CONFIG_HOME="$xdg_config_home" XDG_CACHE_HOME="$xdg_cache_home" \ + ${pkgs.util-linux}/bin/setsid \ ${pkgs.xvfb-run}/bin/xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" \ ${pkgs.brave}/bin/brave \ --user-data-dir="$user_data_dir" \ From b916b76d28dde6320ee2939a2ae7bf3bc3e549a5 Mon Sep 17 00:00:00 2001 From: martin Date: Thu, 28 May 2026 06:16:28 -0500 Subject: [PATCH 11/17] Revert "fix hup" This reverts commit 43d2d75d7df084f11202b07e2a77ba2dc4c7e302. --- nix/codex-resources/default.nix | 1 - 1 file changed, 1 deletion(-) diff --git a/nix/codex-resources/default.nix b/nix/codex-resources/default.nix index cb70200..d1693d6 100644 --- a/nix/codex-resources/default.nix +++ b/nix/codex-resources/default.nix @@ -103,7 +103,6 @@ flake-utils.lib.eachSystem systems ( log_file="$profile_root/brave.log" HOME="$home_dir" XDG_CONFIG_HOME="$xdg_config_home" XDG_CACHE_HOME="$xdg_cache_home" \ - ${pkgs.util-linux}/bin/setsid \ ${pkgs.xvfb-run}/bin/xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" \ ${pkgs.brave}/bin/brave \ --user-data-dir="$user_data_dir" \ From 6d1cda018bbfd68aa795766b129e2f42f557f3ab Mon Sep 17 00:00:00 2001 From: martin Date: Thu, 28 May 2026 06:23:24 -0500 Subject: [PATCH 12/17] Update Linux Chrome skill launch guidance --- nix/codex-resources/patches/chrome-linux-brave-skill.patch | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nix/codex-resources/patches/chrome-linux-brave-skill.patch b/nix/codex-resources/patches/chrome-linux-brave-skill.patch index 7f40336..450d1a0 100644 --- a/nix/codex-resources/patches/chrome-linux-brave-skill.patch +++ b/nix/codex-resources/patches/chrome-linux-brave-skill.patch @@ -11,7 +11,7 @@ +scripts/open-chrome-window.js +``` + -+Then wait 2 seconds and retry the browser-client setup once. ++Then wait 30 seconds after the browser starts before retrying browser-client setup once. The Codex Chrome Extension and native host bridge can take time to install, start, and register their socket after the browser window appears. ### 3. The native host manifest is not installed, or is invalid @@ -20,7 +20,7 @@ ### open-chrome-window.js -This script opens `about:blank` in a Google Chrome window for the same selected Chrome profile used by `check-extension-installed.js`. Use it only after the User gives permission. -+This script opens a browser window for the selected Chrome automation profile. On Linux, the packaged script starts Brave from Nix under Xvfb with a fresh temporary profile configured for the Codex Chrome Extension and native host bridge. Use it only after the User gives permission. ++This script opens a browser window for the selected Chrome automation profile. On Linux, the packaged script starts Brave from Nix under Xvfb with a fresh temporary profile configured for the Codex Chrome Extension and native host bridge. Run the Linux browser process from a long-running background terminal/tool session tied to the current Codex session, and keep that session alive until browser work is finished; if it is launched as a detached one-shot process, Brave/Xvfb and the extension socket may disappear before browser-client can connect. Use it only after the User gives permission. From the plugin root, use `node_repl` to run: From 992a4bf1b81bbe3811d06c3dcbbee203ba9c3f76 Mon Sep 17 00:00:00 2001 From: martin Date: Thu, 28 May 2026 06:31:50 -0500 Subject: [PATCH 13/17] foreground launching --- nix/codex-resources/default.nix | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/nix/codex-resources/default.nix b/nix/codex-resources/default.nix index d1693d6..860cad9 100644 --- a/nix/codex-resources/default.nix +++ b/nix/codex-resources/default.nix @@ -110,11 +110,7 @@ flake-utils.lib.eachSystem systems ( --no-first-run \ --no-default-browser-check \ --disable-dev-shm-usage \ - --new-window about:blank \ - > "$log_file" 2>&1 & - - echo "$!" > "$profile_root/xvfb-run.pid" - echo "Started Brave with profile root: $profile_root" + --new-window about:blank ''; in { From 8000a4d78f69e7b0effcf5516e190bbecfc878e7 Mon Sep 17 00:00:00 2001 From: martin Date: Thu, 28 May 2026 06:48:52 -0500 Subject: [PATCH 14/17] strip down the setup for linux this is clearly vibecoded slopped together at openai wow --- nix/codex-resources/default.nix | 4 +- .../patches/chrome-linux-brave-skill.patch | 226 +++++++++++++++--- 2 files changed, 199 insertions(+), 31 deletions(-) diff --git a/nix/codex-resources/default.nix b/nix/codex-resources/default.nix index 860cad9..8414d36 100644 --- a/nix/codex-resources/default.nix +++ b/nix/codex-resources/default.nix @@ -79,7 +79,7 @@ flake-utils.lib.eachSystem systems ( set -euo pipefail if [[ "$#" -gt 0 ]]; then - echo "Usage: scripts/open-chrome-window.js" >&2 + echo "Usage: scripts/open-chrome-window" >&2 exit 2 fi @@ -154,7 +154,7 @@ flake-utils.lib.eachSystem systems ( chromePluginRoot="$out/plugins/openai-bundled/plugins/chrome" patch --batch --forward --strip 1 --directory "$chromePluginRoot" < ${./patches/chrome-linux-brave-skill.patch} rm "$chromePluginRoot/scripts/open-chrome-window.js" - ln -s ${linuxOpenChromeWindow}/bin/codex-open-chrome-window "$chromePluginRoot/scripts/open-chrome-window.js" + ln -s ${linuxOpenChromeWindow}/bin/codex-open-chrome-window "$chromePluginRoot/scripts/open-chrome-window" '' + pkgs.lib.optionalString (system == "aarch64-darwin") '' install -m755 Codex.app/Contents/Resources/node "$out/node" diff --git a/nix/codex-resources/patches/chrome-linux-brave-skill.patch b/nix/codex-resources/patches/chrome-linux-brave-skill.patch index 450d1a0..b44ac84 100644 --- a/nix/codex-resources/patches/chrome-linux-brave-skill.patch +++ b/nix/codex-resources/patches/chrome-linux-brave-skill.patch @@ -1,38 +1,206 @@ --- a/skills/chrome/SKILL.md +++ b/skills/chrome/SKILL.md -@@ -46,6 +46,14 @@ - Keep the first response short and non-technical unless the user asks for more information. - - If Chrome is not running then ALWAYS ask the User if they would like to launch Chrome. ALWAYS wait for a user response before taking action. -+ -+If the User agrees, run: -+ -+``` -+scripts/open-chrome-window.js -+``` -+ -+Then wait 30 seconds after the browser starts before retrying browser-client setup once. The Codex Chrome Extension and native host bridge can take time to install, start, and register their socket after the browser window appears. - - - ### 3. The native host manifest is not installed, or is invalid -@@ -141,19 +149,13 @@ - - ### open-chrome-window.js - --This script opens `about:blank` in a Google Chrome window for the same selected Chrome profile used by `check-extension-installed.js`. Use it only after the User gives permission. -+This script opens a browser window for the selected Chrome automation profile. On Linux, the packaged script starts Brave from Nix under Xvfb with a fresh temporary profile configured for the Codex Chrome Extension and native host bridge. Run the Linux browser process from a long-running background terminal/tool session tied to the current Codex session, and keep that session alive until browser work is finished; if it is launched as a detached one-shot process, Brave/Xvfb and the extension socket may disappear before browser-client can connect. Use it only after the User gives permission. - - From the plugin root, use `node_repl` to run: - +@@ -11,89 +11,21 @@ + + - Use Chrome directly for browser automation requests and for Chrome setup, detection, repair, or profile checks. + - For bare or general `@chrome` requests, do not ask a clarification question just because the request is ambiguous. Proceed with browser automation in this skill using the `chrome` backend. +-- If communication with the Codex Chrome Extension ultimately fails, even after checks, do not attempt to complete the user's request using applescript, bash commands or any other scripting methods. +-- Do not install or repair the native host yourself. If native host setup appears broken, tell the user to reinstall the Chrome plugin from the Codex plugin UI. + + Before using this skill for the first time in the current conversation context, read the entire `SKILL.md` file in one read. Do not use a partial range such as `sed -n '1,220p'`; read through the end of the file. Do not mention this internal skill-loading step to the user. + +-## Chrome Extension Checks ++## Running Chrome + +-On the first Chrome-backed browser task in a session, try a lightweight browser-client call such as listing open tabs after bootstrap. If the call fails, wait 2 seconds and retry the same lightweight browser-client call once. Any non-error response means the extension is installed and working. ++To launch Chrome, run the following script + +-If browser-client still reports that it cannot communicate with Chrome after that retry, confirm that Chrome is installed, running and that the extension is present in the selected Chrome profile: +- +-From the plugin root, use `node_repl` to run: +- ``` - scripts/open-chrome-window.js +-scripts/chrome-is-running.js --check +-scripts/installed-browsers.js --check +-scripts/check-extension-installed.js --json +-scripts/check-native-host-manifest.js --json ++scripts/open-chrome-window + ``` + +-Depending on the outcome follow the following checks. Be sure to ask the user permission when required, if it is stated in the check. +- +- +-### 1. Chrome is not installed +- +-Keep the first response short and non-technical unless the user asks for more information. +- +-If Chrome is not installed, then inform the user that this plugin only works with the Chrome browser. +- +- +-### 2. Chrome is not running +- +-Keep the first response short and non-technical unless the user asks for more information. +- +-If Chrome is not running then ALWAYS ask the User if they would like to launch Chrome. ALWAYS wait for a user response before taking action. +- +- +-### 3. The native host manifest is not installed, or is invalid +- +-Keep the first response short and non-technical unless the user asks for more information. +- +-Do not install or repair the native host yourself. If native host setup appears broken, tell the user to reinstall the Chrome plugin from the Codex plugin UI. +- +- +-### 4. The Codex Chrome Extension is not installed +- +-Keep the first response short and non-technical unless the user asks for more information. +- +-If the Codex Chrome Extension is missing, tell the user: +- +-`Cannot communicate with the Codex Chrome Extension. Confirm that the extension is installed and enabled in Chrome.` +- +-Ask the User if you can open the Codex Chrome Extension webstore page so they can verify that the extension is installed. ALWAYS wait for a user response before taking action. ALWAYS refer to the extension as the [Codex Chrome Extension](https://chromewebstore.google.com/detail/codex/<>), and not by it's extension ID. +- +-You can construct the URL of the Codex Chrome extension webstore page by appending the `extensionId` from `scripts/extension-id.json` to `https://chromewebstore.google.com/detail/codex/`. +- +- +-### 4. The Codex Chrome Extension is not enabled +- +-Keep the first response short and non-technical unless the user asks for more information. +- +-If the Codex Chrome Extension is not enabled ask the User if you can open the Google Chrome Extension Manager so they can verify that the extension is enabled. ALWAYS wait for a user response before taking action. Always refer to the Google Chrome Extension Manager as [Google Chrome Extension Manager](chrome://extensions/). +- +- +-### 5. Codex Extension is installed and enabled, the manifest file is installed, but communication still fails +- +-Keep the first response short and non-technical unless the user asks for more information. +- +-If Chrome is running and the extension/native-host checks pass, ask the User if you can open a Chrome window for the selected Chrome profile and retry the connection. ALWAYS wait for a user response before taking action. +- +-If the User agrees, run: +- +-``` +-scripts/open-chrome-window.js +-``` +- + Then wait 2 seconds and retry the browser-client setup once. + +-After one successful setup check in a session, do not repeat extension detection unless browser-client reports an extension connection failure. ++This script opens a browser window for the selected Chrome automation profile. Run this process from a long-running background terminal/tool session tied to the current Codex session, and keep that session alive until browser work is finished. + +-If the issue is specifically the native host or extension-backed install path, or if communication still fails after opening a Chrome window and retrying setup once, tell the user to reinstall the Chrome plugin from the Codex plugin UI. Never import or run `scripts/installManifest.mjs` yourself. +- +- + ## Chrome Error handling + + ### File upload errors +@@ -104,91 +36,6 @@ + + `To enable file upload, go to chrome://extensions in Chrome, click Details under the Codex extension, and enable "Allow access to file URLs." See [here](https://developers.openai.com/codex/app/chrome-extension#upload-files) for details.` + +- +-## Commands +- +-### installed-browsers.js +- +-This script reports which browsers are installed. +- +-From the plugin root, use `node_repl` to run: +- +-``` +-scripts/installed-browsers.js +-``` +- +-Use JSON output when another tool or script needs structured data: +- +-``` +-scripts/installed-browsers.js --json +-``` +- +-### chrome-is-running.js +- +-This script checks whether Google Chrome is actively running. It exits `0` when Chrome is running, `1` when Chrome is not running, and `2` for usage or runtime errors. +- +-From the plugin root, use `node_repl` to run: +- +-``` +-scripts/chrome-is-running.js --check +-``` +- +-Use JSON output when another tool or script needs structured data: +- +-``` +-scripts/chrome-is-running.js --json +-``` +- +-### open-chrome-window.js +- +-This script opens `about:blank` in a Google Chrome window for the same selected Chrome profile used by `check-extension-installed.js`. Use it only after the User gives permission. +- +-From the plugin root, use `node_repl` to run: +- +-``` +-scripts/open-chrome-window.js -``` - -Use dry-run JSON output when another tool or script needs to verify the selected launch command without opening Chrome: - - ``` +-``` -scripts/open-chrome-window.js --dry-run --json -``` - - ### check-extension-installed.js - +- +-### check-extension-installed.js +- +-This script checks whether the selected Google Chrome profile has the configured extension registered and present, either from installed version directories or a registered unpacked extension path. It exits `0` when installed and enabled, `1` when installed but not enabled, `2` when not installed, and `3` for usage or runtime errors. +- +-From the plugin root, use `node_repl` to run: +- +-``` +-scripts/check-extension-installed.js +-``` +- +-Use JSON output when another tool or script needs structured data: +- +-``` +-scripts/check-extension-installed.js --json +-``` +- +-The check reads the configured extension ID from `scripts/extension-id.json`. It detects the Chrome profile from `Local State`, then falls back to the highest-numbered `Profile X` or `Default` directory with `Preferences`. For debugging or tests, override profile selection with `CODEX_CHROME_USER_DATA_DIR=/path/to/chrome-root` or `CODEX_CHROME_PREFERENCES_PATH=/path/to/Profile/Preferences`. +- +-### check-native-host-manifest.js +- +-This script checks whether the Chrome Native Messaging Host manifest exists for the configured native host name and allows the Chrome extension ID from `scripts/extension-id.json`. On Windows it also checks the Chrome NativeMessagingHosts registry key. It exits `0` when correct, `1` when missing or incorrect, and `2` for usage or runtime errors. +- +-From the plugin root, use `node_repl` to run: +- +-``` +-scripts/check-native-host-manifest.js +-``` +- +-Use JSON output when another tool or script needs structured data: +- +-``` +-scripts/check-native-host-manifest.js --json +-``` +- + ## Chrome Safety + + - Do not inspect browser cookies, local storage, profiles, passwords, or session stores. +@@ -749,7 +596,7 @@ + interface CUAAPI { + click(options: ClickOptions): Promise; // Click at a coordinate in the current viewport. + double_click(options: DoubleClickOptions): Promise; // Double click at a coordinate in the current viewport. +- ++ + drag(options: DragOptions): Promise; // Drag from a point to a point by the provided path. + keypress(options: KeypressOptions): Promise; // Press control characters at the current focused element (focus it first via click/dblclick). + move(options: MoveOptions): Promise; // Move the mouse to a point by the provided x and y coordinates. +@@ -760,7 +607,7 @@ + interface DomCUAAPI { + click(options: DomClickOptions): Promise; // Click a DOM node by its id from the visible DOM snapshot. + double_click(options: DomClickOptions): Promise; // Double-click a DOM node by its id. +- ++ + get_visible_dom(): Promise; // Return a filtered DOM with node ids for interactable elements. + keypress(options: DomKeypressOptions): Promise; // Press control characters at the currently focused element (focus it first via click/dblclick). + scroll(options: DomScrollOptions): Promise; // Scroll either the page or a specific node (if node_id provided) by deltas. From a2ad04edbeac54e12d5708eb39a2709627aafb36 Mon Sep 17 00:00:00 2001 From: martin Date: Thu, 28 May 2026 06:51:35 -0500 Subject: [PATCH 15/17] fix application --- .../patches/chrome-linux-brave-skill.patch | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/nix/codex-resources/patches/chrome-linux-brave-skill.patch b/nix/codex-resources/patches/chrome-linux-brave-skill.patch index b44ac84..d85dbea 100644 --- a/nix/codex-resources/patches/chrome-linux-brave-skill.patch +++ b/nix/codex-resources/patches/chrome-linux-brave-skill.patch @@ -1,20 +1,20 @@ --- a/skills/chrome/SKILL.md +++ b/skills/chrome/SKILL.md @@ -11,89 +11,21 @@ - + - Use Chrome directly for browser automation requests and for Chrome setup, detection, repair, or profile checks. - For bare or general `@chrome` requests, do not ask a clarification question just because the request is ambiguous. Proceed with browser automation in this skill using the `chrome` backend. -- If communication with the Codex Chrome Extension ultimately fails, even after checks, do not attempt to complete the user's request using applescript, bash commands or any other scripting methods. -- Do not install or repair the native host yourself. If native host setup appears broken, tell the user to reinstall the Chrome plugin from the Codex plugin UI. - + Before using this skill for the first time in the current conversation context, read the entire `SKILL.md` file in one read. Do not use a partial range such as `sed -n '1,220p'`; read through the end of the file. Do not mention this internal skill-loading step to the user. - + -## Chrome Extension Checks +## Running Chrome - + -On the first Chrome-backed browser task in a session, try a lightweight browser-client call such as listing open tabs after bootstrap. If the call fails, wait 2 seconds and retry the same lightweight browser-client call once. Any non-error response means the extension is installed and working. +To launch Chrome, run the following script - + -If browser-client still reports that it cannot communicate with Chrome after that retry, confirm that Chrome is installed, running and that the extension is present in the selected Chrome profile: - -From the plugin root, use `node_repl` to run: @@ -26,7 +26,7 @@ -scripts/check-native-host-manifest.js --json +scripts/open-chrome-window ``` - + -Depending on the outcome follow the following checks. Be sure to ask the user permission when required, if it is stated in the check. - - @@ -57,7 +57,7 @@ - -If the Codex Chrome Extension is missing, tell the user: - --`Cannot communicate with the Codex Chrome Extension. Confirm that the extension is installed and enabled in Chrome.` +-`Cannot communicate with the Codex Chrome Extension. Confirm that the extension is installed and enabled in Chrome.` - -Ask the User if you can open the Codex Chrome Extension webstore page so they can verify that the extension is installed. ALWAYS wait for a user response before taking action. ALWAYS refer to the extension as the [Codex Chrome Extension](https://chromewebstore.google.com/detail/codex/<>), and not by it's extension ID. - @@ -84,20 +84,20 @@ -``` - Then wait 2 seconds and retry the browser-client setup once. - + -After one successful setup check in a session, do not repeat extension detection unless browser-client reports an extension connection failure. +This script opens a browser window for the selected Chrome automation profile. Run this process from a long-running background terminal/tool session tied to the current Codex session, and keep that session alive until browser work is finished. - + -If the issue is specifically the native host or extension-backed install path, or if communication still fails after opening a Chrome window and retrying setup once, tell the user to reinstall the Chrome plugin from the Codex plugin UI. Never import or run `scripts/installManifest.mjs` yourself. - - ## Chrome Error handling - + ### File upload errors @@ -104,91 +36,6 @@ - + `To enable file upload, go to chrome://extensions in Chrome, click Details under the Codex extension, and enable "Allow access to file URLs." See [here](https://developers.openai.com/codex/app/chrome-extension#upload-files) for details.` - + - -## Commands - @@ -184,13 +184,13 @@ -``` - ## Chrome Safety - + - Do not inspect browser cookies, local storage, profiles, passwords, or session stores. @@ -749,7 +596,7 @@ interface CUAAPI { click(options: ClickOptions): Promise; // Click at a coordinate in the current viewport. double_click(options: DoubleClickOptions): Promise; // Double click at a coordinate in the current viewport. -- +- + drag(options: DragOptions): Promise; // Drag from a point to a point by the provided path. keypress(options: KeypressOptions): Promise; // Press control characters at the current focused element (focus it first via click/dblclick). @@ -199,7 +199,7 @@ interface DomCUAAPI { click(options: DomClickOptions): Promise; // Click a DOM node by its id from the visible DOM snapshot. double_click(options: DomClickOptions): Promise; // Double-click a DOM node by its id. -- +- + get_visible_dom(): Promise; // Return a filtered DOM with node ids for interactable elements. keypress(options: DomKeypressOptions): Promise; // Press control characters at the currently focused element (focus it first via click/dblclick). From 5b3b390a2ca4179618ca8958552e0fbf62a0bff0 Mon Sep 17 00:00:00 2001 From: martin Date: Thu, 28 May 2026 07:08:16 -0500 Subject: [PATCH 16/17] fix script path --- nix/codex-resources/default.nix | 3 ++- nix/codex-resources/patches/chrome-linux-brave-skill.patch | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/nix/codex-resources/default.nix b/nix/codex-resources/default.nix index 8414d36..3d3ad49 100644 --- a/nix/codex-resources/default.nix +++ b/nix/codex-resources/default.nix @@ -153,8 +153,9 @@ flake-utils.lib.eachSystem systems ( + pkgs.lib.optionalString (system == "x86_64-linux") '' chromePluginRoot="$out/plugins/openai-bundled/plugins/chrome" patch --batch --forward --strip 1 --directory "$chromePluginRoot" < ${./patches/chrome-linux-brave-skill.patch} + substituteInPlace "$chromePluginRoot/skills/chrome/SKILL.md" \ + --replace-fail '@openChromeWindow@' '${linuxOpenChromeWindow}/bin/codex-open-chrome-window' rm "$chromePluginRoot/scripts/open-chrome-window.js" - ln -s ${linuxOpenChromeWindow}/bin/codex-open-chrome-window "$chromePluginRoot/scripts/open-chrome-window" '' + pkgs.lib.optionalString (system == "aarch64-darwin") '' install -m755 Codex.app/Contents/Resources/node "$out/node" diff --git a/nix/codex-resources/patches/chrome-linux-brave-skill.patch b/nix/codex-resources/patches/chrome-linux-brave-skill.patch index d85dbea..1ab0a52 100644 --- a/nix/codex-resources/patches/chrome-linux-brave-skill.patch +++ b/nix/codex-resources/patches/chrome-linux-brave-skill.patch @@ -24,7 +24,7 @@ -scripts/installed-browsers.js --check -scripts/check-extension-installed.js --json -scripts/check-native-host-manifest.js --json -+scripts/open-chrome-window ++@openChromeWindow@ ``` -Depending on the outcome follow the following checks. Be sure to ask the user permission when required, if it is stated in the check. From 6ce31bee57a41e92ff3f12c73a3a13d805fac7d4 Mon Sep 17 00:00:00 2001 From: martin Date: Thu, 28 May 2026 07:35:08 -0500 Subject: [PATCH 17/17] add positioning flags --- nix/codex-resources/default.nix | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nix/codex-resources/default.nix b/nix/codex-resources/default.nix index 3d3ad49..aed6d23 100644 --- a/nix/codex-resources/default.nix +++ b/nix/codex-resources/default.nix @@ -110,6 +110,10 @@ flake-utils.lib.eachSystem systems ( --no-first-run \ --no-default-browser-check \ --disable-dev-shm-usage \ + --force-device-scale-factor=1 \ + --start-maximized \ + --window-position=0,0 \ + --window-size=1920,1080 \ --new-window about:blank ''; in