From ee4138c3638a38834b3c10a764350b1417500d02 Mon Sep 17 00:00:00 2001 From: Jeremy Lewi Date: Mon, 13 Apr 2026 09:51:19 -0700 Subject: [PATCH] refactor: make code-mode crate wasm-safe Gate the native V8 dependency out of wasm builds and provide a wasm-specific default CodeModeService that preserves the shared request/response/runtime trait surface while returning clear errors unless a browser runtime is injected. Keep the public code-mode types available on wasm and mark the V8-backed runtime internals as native-only so codex-code-mode now checks cleanly for wasm32-unknown-unknown. --- codex-rs/code-mode/Cargo.toml | 4 +- codex-rs/code-mode/src/description.rs | 2 + codex-rs/code-mode/src/lib.rs | 1 + codex-rs/code-mode/src/runtime/mod.rs | 32 +++++-- codex-rs/code-mode/src/service_wasm.rs | 119 +++++++++++++++++++++++++ 5 files changed, 152 insertions(+), 6 deletions(-) create mode 100644 codex-rs/code-mode/src/service_wasm.rs diff --git a/codex-rs/code-mode/Cargo.toml b/codex-rs/code-mode/Cargo.toml index e821ca0e4d..707f0e788f 100644 --- a/codex-rs/code-mode/Cargo.toml +++ b/codex-rs/code-mode/Cargo.toml @@ -19,7 +19,9 @@ serde_json = { workspace = true } tokio = { workspace = true, features = ["macros", "rt", "sync", "time"] } tokio-util = { workspace = true, features = ["rt"] } tracing = { workspace = true } -v8 = { workspace = true } [dev-dependencies] pretty_assertions = { workspace = true } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +v8 = { workspace = true } diff --git a/codex-rs/code-mode/src/description.rs b/codex-rs/code-mode/src/description.rs index 3a87fef092..aa6697d6a7 100644 --- a/codex-rs/code-mode/src/description.rs +++ b/codex-rs/code-mode/src/description.rs @@ -225,6 +225,7 @@ pub fn augment_tool_definition(mut definition: ToolDefinition) -> ToolDefinition definition } +#[cfg(not(target_arch = "wasm32"))] pub fn enabled_tool_metadata(definition: &ToolDefinition) -> EnabledToolMetadata { EnabledToolMetadata { tool_name: definition.name.clone(), @@ -234,6 +235,7 @@ pub fn enabled_tool_metadata(definition: &ToolDefinition) -> EnabledToolMetadata } } +#[cfg(not(target_arch = "wasm32"))] #[derive(Clone, Debug, Eq, PartialEq, Serialize)] pub struct EnabledToolMetadata { pub tool_name: String, diff --git a/codex-rs/code-mode/src/lib.rs b/codex-rs/code-mode/src/lib.rs index 27488d4707..e37d686718 100644 --- a/codex-rs/code-mode/src/lib.rs +++ b/codex-rs/code-mode/src/lib.rs @@ -1,6 +1,7 @@ mod description; mod response; mod runtime; +#[cfg_attr(target_arch = "wasm32", path = "service_wasm.rs")] mod service; pub use description::CODE_MODE_PRAGMA_PREFIX; diff --git a/codex-rs/code-mode/src/runtime/mod.rs b/codex-rs/code-mode/src/runtime/mod.rs index df90eda673..9d4fbe814f 100644 --- a/codex-rs/code-mode/src/runtime/mod.rs +++ b/codex-rs/code-mode/src/runtime/mod.rs @@ -1,24 +1,35 @@ +#[cfg(not(target_arch = "wasm32"))] mod callbacks; +#[cfg(not(target_arch = "wasm32"))] mod globals; +#[cfg(not(target_arch = "wasm32"))] mod module_loader; +#[cfg(not(target_arch = "wasm32"))] mod value; use std::collections::HashMap; -use std::sync::OnceLock; -use std::sync::mpsc as std_mpsc; -use std::thread; use serde_json::Value as JsonValue; -use tokio::sync::mpsc; +#[cfg(not(target_arch = "wasm32"))] use crate::description::EnabledToolMetadata; use crate::description::ToolDefinition; +#[cfg(not(target_arch = "wasm32"))] use crate::description::enabled_tool_metadata; use crate::response::FunctionCallOutputContentItem; +#[cfg(not(target_arch = "wasm32"))] +use std::sync::OnceLock; +#[cfg(not(target_arch = "wasm32"))] +use std::sync::mpsc as std_mpsc; +#[cfg(not(target_arch = "wasm32"))] +use std::thread; +#[cfg(not(target_arch = "wasm32"))] +use tokio::sync::mpsc; pub const DEFAULT_EXEC_YIELD_TIME_MS: u64 = 10_000; pub const DEFAULT_WAIT_YIELD_TIME_MS: u64 = 10_000; pub const DEFAULT_MAX_OUTPUT_TOKENS_PER_EXEC_CALL: usize = 10_000; +#[cfg(not(target_arch = "wasm32"))] const EXIT_SENTINEL: &str = "__codex_code_mode_exit__"; #[derive(Clone, Debug)] @@ -56,6 +67,7 @@ pub enum RuntimeResponse { }, } +#[cfg(not(target_arch = "wasm32"))] #[derive(Debug)] pub(crate) enum TurnMessage { ToolCall { @@ -71,6 +83,7 @@ pub(crate) enum TurnMessage { }, } +#[cfg(not(target_arch = "wasm32"))] #[derive(Debug)] pub(crate) enum RuntimeCommand { ToolResponse { id: String, result: JsonValue }, @@ -78,6 +91,7 @@ pub(crate) enum RuntimeCommand { Terminate, } +#[cfg(not(target_arch = "wasm32"))] #[derive(Debug)] pub(crate) enum RuntimeEvent { Started, @@ -98,6 +112,7 @@ pub(crate) enum RuntimeEvent { }, } +#[cfg(not(target_arch = "wasm32"))] pub(crate) fn spawn_runtime( request: ExecuteRequest, event_tx: mpsc::UnboundedSender, @@ -126,6 +141,7 @@ pub(crate) fn spawn_runtime( Ok((command_tx, isolate_handle)) } +#[cfg(not(target_arch = "wasm32"))] #[derive(Clone)] struct RuntimeConfig { tool_call_id: String, @@ -134,6 +150,7 @@ struct RuntimeConfig { stored_values: HashMap, } +#[cfg(not(target_arch = "wasm32"))] pub(super) struct RuntimeState { event_tx: mpsc::UnboundedSender, pending_tool_calls: HashMap>, @@ -144,6 +161,7 @@ pub(super) struct RuntimeState { exit_requested: bool, } +#[cfg(not(target_arch = "wasm32"))] pub(super) enum CompletionState { Pending, Completed { @@ -152,6 +170,7 @@ pub(super) enum CompletionState { }, } +#[cfg(not(target_arch = "wasm32"))] fn initialize_v8() { static PLATFORM: OnceLock> = OnceLock::new(); @@ -163,6 +182,7 @@ fn initialize_v8() { }); } +#[cfg(not(target_arch = "wasm32"))] fn run_runtime( config: RuntimeConfig, event_tx: mpsc::UnboundedSender, @@ -264,6 +284,7 @@ fn run_runtime( } } +#[cfg(not(target_arch = "wasm32"))] fn capture_scope_send_error( scope: &mut v8::PinScope<'_, '_>, event_tx: &mpsc::UnboundedSender, @@ -277,6 +298,7 @@ fn capture_scope_send_error( send_result(event_tx, stored_values, error_text); } +#[cfg(not(target_arch = "wasm32"))] fn send_result( event_tx: &mpsc::UnboundedSender, stored_values: HashMap, @@ -288,7 +310,7 @@ fn send_result( }); } -#[cfg(test)] +#[cfg(all(test, not(target_arch = "wasm32")))] mod tests { use std::collections::HashMap; use std::time::Duration; diff --git a/codex-rs/code-mode/src/service_wasm.rs b/codex-rs/code-mode/src/service_wasm.rs new file mode 100644 index 0000000000..59dc9c1c7d --- /dev/null +++ b/codex-rs/code-mode/src/service_wasm.rs @@ -0,0 +1,119 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use async_trait::async_trait; +use serde_json::Value as JsonValue; +use tokio::sync::Mutex; +use tokio_util::sync::CancellationToken; + +use crate::ExecuteRequest; +use crate::RuntimeResponse; +use crate::WaitRequest; + +#[async_trait] +pub trait CodeModeTurnHost: Send + Sync { + async fn invoke_tool( + &self, + tool_name: String, + input: Option, + cancellation_token: CancellationToken, + ) -> Result; + + async fn notify(&self, call_id: String, cell_id: String, text: String) -> Result<(), String>; +} + +#[async_trait] +pub trait CodeModeRuntime: Send + Sync { + async fn stored_values(&self) -> HashMap; + + async fn replace_stored_values(&self, values: HashMap); + + async fn execute(&self, request: ExecuteRequest) -> Result; + + async fn wait(&self, request: WaitRequest) -> Result; + + fn start_turn_worker( + &self, + host: Arc, + ) -> Box; +} + +pub struct CodeModeService { + stored_values: Mutex>, +} + +impl CodeModeService { + pub fn new() -> Self { + Self { + stored_values: Mutex::new(HashMap::new()), + } + } + + pub async fn stored_values(&self) -> HashMap { + self.stored_values.lock().await.clone() + } + + pub async fn replace_stored_values(&self, values: HashMap) { + *self.stored_values.lock().await = values; + } + + pub async fn execute(&self, _request: ExecuteRequest) -> Result { + Err( + "native code mode runtime is unavailable on wasm32; inject a browser CodeModeRuntime" + .to_string(), + ) + } + + pub async fn wait(&self, request: WaitRequest) -> Result { + Ok(RuntimeResponse::Result { + cell_id: request.cell_id, + content_items: Vec::new(), + stored_values: self.stored_values().await, + error_text: Some( + "code mode wait is unavailable on wasm32 without an injected runtime".to_string(), + ), + }) + } + + pub fn start_turn_worker(&self, _host: Arc) -> CodeModeTurnWorker { + CodeModeTurnWorker + } +} + +impl Default for CodeModeService { + fn default() -> Self { + Self::new() + } +} + +pub struct CodeModeTurnWorker; + +pub trait CodeModeTurnWorkerHandle: Send {} + +impl CodeModeTurnWorkerHandle for CodeModeTurnWorker {} + +#[async_trait] +impl CodeModeRuntime for CodeModeService { + async fn stored_values(&self) -> HashMap { + CodeModeService::stored_values(self).await + } + + async fn replace_stored_values(&self, values: HashMap) { + CodeModeService::replace_stored_values(self, values).await; + } + + async fn execute(&self, request: ExecuteRequest) -> Result { + CodeModeService::execute(self, request).await + } + + async fn wait(&self, request: WaitRequest) -> Result { + CodeModeService::wait(self, request).await + } + + fn start_turn_worker( + &self, + host: Arc, + ) -> Box { + Box::new(CodeModeService::start_turn_worker(self, host)) + } +}