From a050c5ee91c13751a6d06a39f7ddcede392272ab Mon Sep 17 00:00:00 2001 From: Jeremy lewi Date: Tue, 14 Apr 2026 14:42:28 -0700 Subject: [PATCH] refactor: add wasm-safe shims for host-only runtime crates --- codex-rs/analytics/Cargo.toml | 6 +- codex-rs/analytics/src/lib.rs | 22 +- codex-rs/analytics/src/native.rs | 17 + codex-rs/analytics/src/wasm.rs | 170 ++++ codex-rs/exec-server/Cargo.toml | 15 +- .../exec-server/src/bin/codex-exec-server.rs | 8 + codex-rs/exec-server/src/lib.rs | 72 +- codex-rs/exec-server/src/native.rs | 67 ++ codex-rs/exec-server/src/wasm.rs | 415 +++++++++ codex-rs/network-proxy/Cargo.toml | 13 +- codex-rs/network-proxy/src/lib.rs | 59 +- codex-rs/network-proxy/src/native.rs | 54 ++ codex-rs/network-proxy/src/wasm.rs | 863 ++++++++++++++++++ codex-rs/rmcp-client/Cargo.toml | 56 +- codex-rs/rmcp-client/src/lib.rs | 36 +- codex-rs/rmcp-client/src/native.rs | 31 + codex-rs/rmcp-client/src/wasm.rs | 340 +++++++ 17 files changed, 2051 insertions(+), 193 deletions(-) create mode 100644 codex-rs/analytics/src/native.rs create mode 100644 codex-rs/analytics/src/wasm.rs create mode 100644 codex-rs/exec-server/src/native.rs create mode 100644 codex-rs/exec-server/src/wasm.rs create mode 100644 codex-rs/network-proxy/src/native.rs create mode 100644 codex-rs/network-proxy/src/wasm.rs create mode 100644 codex-rs/rmcp-client/src/native.rs create mode 100644 codex-rs/rmcp-client/src/wasm.rs diff --git a/codex-rs/analytics/Cargo.toml b/codex-rs/analytics/Cargo.toml index 63bf63c911..240f574400 100644 --- a/codex-rs/analytics/Cargo.toml +++ b/codex-rs/analytics/Cargo.toml @@ -15,16 +15,18 @@ workspace = true [dependencies] codex-app-server-protocol = { workspace = true } codex-git-utils = { workspace = true } -codex-login = { workspace = true } codex-plugin = { workspace = true } codex-protocol = { workspace = true } serde = { workspace = true, features = ["derive"] } sha1 = { workspace = true } +tracing = { workspace = true, features = ["log"] } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +codex-login = { workspace = true } tokio = { workspace = true, features = [ "macros", "rt-multi-thread", ] } -tracing = { workspace = true, features = ["log"] } [dev-dependencies] pretty_assertions = { workspace = true } diff --git a/codex-rs/analytics/src/lib.rs b/codex-rs/analytics/src/lib.rs index 5b15d2830a..22186e8715 100644 --- a/codex-rs/analytics/src/lib.rs +++ b/codex-rs/analytics/src/lib.rs @@ -1,17 +1,7 @@ -mod analytics_client; +#[cfg(not(target_arch = "wasm32"))] +include!("native.rs"); -pub use analytics_client::AnalyticsEventsClient; -pub use analytics_client::AnalyticsFact; -pub use analytics_client::AnalyticsReducer; -pub use analytics_client::AppInvocation; -pub use analytics_client::AppMentionedInput; -pub use analytics_client::AppUsedInput; -pub use analytics_client::CustomAnalyticsFact; -pub use analytics_client::InvocationType; -pub use analytics_client::PluginState; -pub use analytics_client::PluginStateChangedInput; -pub use analytics_client::PluginUsedInput; -pub use analytics_client::SkillInvocation; -pub use analytics_client::SkillInvokedInput; -pub use analytics_client::TrackEventsContext; -pub use analytics_client::build_track_events_context; +#[cfg(target_arch = "wasm32")] +mod wasm; +#[cfg(target_arch = "wasm32")] +pub use wasm::*; diff --git a/codex-rs/analytics/src/native.rs b/codex-rs/analytics/src/native.rs new file mode 100644 index 0000000000..5b15d2830a --- /dev/null +++ b/codex-rs/analytics/src/native.rs @@ -0,0 +1,17 @@ +mod analytics_client; + +pub use analytics_client::AnalyticsEventsClient; +pub use analytics_client::AnalyticsFact; +pub use analytics_client::AnalyticsReducer; +pub use analytics_client::AppInvocation; +pub use analytics_client::AppMentionedInput; +pub use analytics_client::AppUsedInput; +pub use analytics_client::CustomAnalyticsFact; +pub use analytics_client::InvocationType; +pub use analytics_client::PluginState; +pub use analytics_client::PluginStateChangedInput; +pub use analytics_client::PluginUsedInput; +pub use analytics_client::SkillInvocation; +pub use analytics_client::SkillInvokedInput; +pub use analytics_client::TrackEventsContext; +pub use analytics_client::build_track_events_context; diff --git a/codex-rs/analytics/src/wasm.rs b/codex-rs/analytics/src/wasm.rs new file mode 100644 index 0000000000..c861347c12 --- /dev/null +++ b/codex-rs/analytics/src/wasm.rs @@ -0,0 +1,170 @@ +use codex_plugin::PluginTelemetryMetadata; +use codex_protocol::protocol::SkillScope; +use serde::Serialize; +use std::path::PathBuf; +use std::sync::Arc; + +#[derive(Clone)] +pub struct TrackEventsContext { + pub model_slug: String, + pub thread_id: String, + pub turn_id: String, +} + +pub fn build_track_events_context( + model_slug: String, + thread_id: String, + turn_id: String, +) -> TrackEventsContext { + TrackEventsContext { + model_slug, + thread_id, + turn_id, + } +} + +#[derive(Clone, Debug)] +pub struct SkillInvocation { + pub skill_name: String, + pub skill_scope: SkillScope, + pub skill_path: PathBuf, + pub invocation_type: InvocationType, +} + +#[derive(Clone, Copy, Debug, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum InvocationType { + Explicit, + Implicit, +} + +pub struct AppInvocation { + pub connector_id: Option, + pub app_name: Option, + pub invocation_type: Option, +} + +pub enum AnalyticsFact { + Initialize { + connection_id: u64, + params: codex_app_server_protocol::InitializeParams, + }, + Request { + connection_id: u64, + request_id: codex_app_server_protocol::RequestId, + request: Box, + }, + Response { + connection_id: u64, + response: Box, + }, + Notification(Box), + Custom(CustomAnalyticsFact), +} + +pub enum CustomAnalyticsFact { + SkillInvoked(SkillInvokedInput), + AppMentioned(AppMentionedInput), + AppUsed(AppUsedInput), + PluginUsed(PluginUsedInput), + PluginStateChanged(PluginStateChangedInput), +} + +pub struct SkillInvokedInput { + pub tracking: TrackEventsContext, + pub invocations: Vec, +} + +pub struct AppMentionedInput { + pub tracking: TrackEventsContext, + pub mentions: Vec, +} + +pub struct AppUsedInput { + pub tracking: TrackEventsContext, + pub app: AppInvocation, +} + +pub struct PluginUsedInput { + pub tracking: TrackEventsContext, + pub plugin: PluginTelemetryMetadata, +} + +pub struct PluginStateChangedInput { + pub plugin: PluginTelemetryMetadata, + pub state: PluginState, +} + +#[derive(Clone, Copy)] +pub enum PluginState { + Installed, + Uninstalled, + Enabled, + Disabled, +} + +#[derive(Default)] +pub struct AnalyticsReducer; + +#[derive(Clone, Default)] +pub struct AnalyticsEventsClient { + analytics_enabled: Option, + _marker: Arc<()>, +} + +impl AnalyticsEventsClient { + pub fn new( + _auth_manager: Arc, + _base_url: String, + analytics_enabled: Option, + ) -> Self { + Self { + analytics_enabled, + _marker: Arc::new(()), + } + } + + pub fn track_skill_invocations( + &self, + _tracking: TrackEventsContext, + _invocations: Vec, + ) { + let _ = self.analytics_enabled; + } + + pub fn track_app_mentioned( + &self, + _tracking: TrackEventsContext, + _mentions: Vec, + ) { + let _ = self.analytics_enabled; + } + + pub fn track_app_used(&self, _tracking: TrackEventsContext, _app: AppInvocation) { + let _ = self.analytics_enabled; + } + + pub fn track_plugin_used( + &self, + _tracking: TrackEventsContext, + _plugin: PluginTelemetryMetadata, + ) { + let _ = self.analytics_enabled; + } + + pub fn track_plugin_installed(&self, _plugin: PluginTelemetryMetadata) { + let _ = self.analytics_enabled; + } + + pub fn track_plugin_uninstalled(&self, _plugin: PluginTelemetryMetadata) { + let _ = self.analytics_enabled; + } + + pub fn track_plugin_enabled(&self, _plugin: PluginTelemetryMetadata) { + let _ = self.analytics_enabled; + } + + pub fn track_plugin_disabled(&self, _plugin: PluginTelemetryMetadata) { + let _ = self.analytics_enabled; + } +} diff --git a/codex-rs/exec-server/Cargo.toml b/codex-rs/exec-server/Cargo.toml index f064a82dca..7f2688a109 100644 --- a/codex-rs/exec-server/Cargo.toml +++ b/codex-rs/exec-server/Cargo.toml @@ -15,17 +15,20 @@ path = "src/bin/codex-exec-server.rs" workspace = true [dependencies] -arc-swap = { workspace = true } async-trait = { workspace = true } base64 = { workspace = true } -clap = { workspace = true, features = ["derive"] } codex-app-server-protocol = { workspace = true } codex-utils-absolute-path = { workspace = true } -codex-utils-pty = { workspace = true } -futures = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } thiserror = { workspace = true } +tracing = { workspace = true } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +arc-swap = { workspace = true } +clap = { workspace = true, features = ["derive"] } +codex-utils-pty = { workspace = true } +futures = { workspace = true } tokio = { workspace = true, features = [ "fs", "io-std", @@ -38,7 +41,9 @@ tokio = { workspace = true, features = [ "time", ] } tokio-tungstenite = { workspace = true } -tracing = { workspace = true } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +tokio = { workspace = true, features = ["io-util", "macros", "rt", "sync"] } [dev-dependencies] anyhow = { workspace = true } diff --git a/codex-rs/exec-server/src/bin/codex-exec-server.rs b/codex-rs/exec-server/src/bin/codex-exec-server.rs index 82fa9ec00f..680453699d 100644 --- a/codex-rs/exec-server/src/bin/codex-exec-server.rs +++ b/codex-rs/exec-server/src/bin/codex-exec-server.rs @@ -1,5 +1,7 @@ +#[cfg(not(target_arch = "wasm32"))] use clap::Parser; +#[cfg(not(target_arch = "wasm32"))] #[derive(Debug, Parser)] struct ExecServerArgs { /// Transport endpoint URL. Supported values: `ws://IP:PORT` (default). @@ -11,8 +13,14 @@ struct ExecServerArgs { listen: String, } +#[cfg(not(target_arch = "wasm32"))] #[tokio::main] async fn main() -> Result<(), Box> { let args = ExecServerArgs::parse(); codex_exec_server::run_main_with_listen_url(&args.listen).await } + +#[cfg(target_arch = "wasm32")] +fn main() { + panic!("codex-exec-server binary is unavailable on wasm32"); +} diff --git a/codex-rs/exec-server/src/lib.rs b/codex-rs/exec-server/src/lib.rs index 77834c6143..22186e8715 100644 --- a/codex-rs/exec-server/src/lib.rs +++ b/codex-rs/exec-server/src/lib.rs @@ -1,67 +1,7 @@ -mod client; -mod client_api; -mod connection; -mod environment; -mod file_system; -mod local_file_system; -mod local_process; -mod process; -mod process_id; -mod protocol; -mod remote_file_system; -mod remote_process; -mod rpc; -mod server; +#[cfg(not(target_arch = "wasm32"))] +include!("native.rs"); -pub use client::ExecServerClient; -pub use client::ExecServerError; -pub use client_api::ExecServerClientConnectOptions; -pub use client_api::RemoteExecServerConnectArgs; -pub use codex_app_server_protocol::FsCopyParams; -pub use codex_app_server_protocol::FsCopyResponse; -pub use codex_app_server_protocol::FsCreateDirectoryParams; -pub use codex_app_server_protocol::FsCreateDirectoryResponse; -pub use codex_app_server_protocol::FsGetMetadataParams; -pub use codex_app_server_protocol::FsGetMetadataResponse; -pub use codex_app_server_protocol::FsReadDirectoryParams; -pub use codex_app_server_protocol::FsReadDirectoryResponse; -pub use codex_app_server_protocol::FsReadFileParams; -pub use codex_app_server_protocol::FsReadFileResponse; -pub use codex_app_server_protocol::FsRemoveParams; -pub use codex_app_server_protocol::FsRemoveResponse; -pub use codex_app_server_protocol::FsWriteFileParams; -pub use codex_app_server_protocol::FsWriteFileResponse; -pub use environment::CODEX_EXEC_SERVER_URL_ENV_VAR; -pub use environment::Environment; -pub use environment::EnvironmentManager; -pub use environment::ExecutorEnvironment; -pub use file_system::CopyOptions; -pub use file_system::CreateDirectoryOptions; -pub use file_system::ExecutorFileSystem; -pub use file_system::FileMetadata; -pub use file_system::FileSystemResult; -pub use file_system::ReadDirectoryEntry; -pub use file_system::RemoveOptions; -pub use process::ExecBackend; -pub use process::ExecProcess; -pub use process::StartedExecProcess; -pub use process_id::ProcessId; -pub use protocol::ExecClosedNotification; -pub use protocol::ExecExitedNotification; -pub use protocol::ExecOutputDeltaNotification; -pub use protocol::ExecOutputStream; -pub use protocol::ExecParams; -pub use protocol::ExecResponse; -pub use protocol::InitializeParams; -pub use protocol::InitializeResponse; -pub use protocol::ReadParams; -pub use protocol::ReadResponse; -pub use protocol::TerminateParams; -pub use protocol::TerminateResponse; -pub use protocol::WriteParams; -pub use protocol::WriteResponse; -pub use protocol::WriteStatus; -pub use server::DEFAULT_LISTEN_URL; -pub use server::ExecServerListenUrlParseError; -pub use server::run_main; -pub use server::run_main_with_listen_url; +#[cfg(target_arch = "wasm32")] +mod wasm; +#[cfg(target_arch = "wasm32")] +pub use wasm::*; diff --git a/codex-rs/exec-server/src/native.rs b/codex-rs/exec-server/src/native.rs new file mode 100644 index 0000000000..77834c6143 --- /dev/null +++ b/codex-rs/exec-server/src/native.rs @@ -0,0 +1,67 @@ +mod client; +mod client_api; +mod connection; +mod environment; +mod file_system; +mod local_file_system; +mod local_process; +mod process; +mod process_id; +mod protocol; +mod remote_file_system; +mod remote_process; +mod rpc; +mod server; + +pub use client::ExecServerClient; +pub use client::ExecServerError; +pub use client_api::ExecServerClientConnectOptions; +pub use client_api::RemoteExecServerConnectArgs; +pub use codex_app_server_protocol::FsCopyParams; +pub use codex_app_server_protocol::FsCopyResponse; +pub use codex_app_server_protocol::FsCreateDirectoryParams; +pub use codex_app_server_protocol::FsCreateDirectoryResponse; +pub use codex_app_server_protocol::FsGetMetadataParams; +pub use codex_app_server_protocol::FsGetMetadataResponse; +pub use codex_app_server_protocol::FsReadDirectoryParams; +pub use codex_app_server_protocol::FsReadDirectoryResponse; +pub use codex_app_server_protocol::FsReadFileParams; +pub use codex_app_server_protocol::FsReadFileResponse; +pub use codex_app_server_protocol::FsRemoveParams; +pub use codex_app_server_protocol::FsRemoveResponse; +pub use codex_app_server_protocol::FsWriteFileParams; +pub use codex_app_server_protocol::FsWriteFileResponse; +pub use environment::CODEX_EXEC_SERVER_URL_ENV_VAR; +pub use environment::Environment; +pub use environment::EnvironmentManager; +pub use environment::ExecutorEnvironment; +pub use file_system::CopyOptions; +pub use file_system::CreateDirectoryOptions; +pub use file_system::ExecutorFileSystem; +pub use file_system::FileMetadata; +pub use file_system::FileSystemResult; +pub use file_system::ReadDirectoryEntry; +pub use file_system::RemoveOptions; +pub use process::ExecBackend; +pub use process::ExecProcess; +pub use process::StartedExecProcess; +pub use process_id::ProcessId; +pub use protocol::ExecClosedNotification; +pub use protocol::ExecExitedNotification; +pub use protocol::ExecOutputDeltaNotification; +pub use protocol::ExecOutputStream; +pub use protocol::ExecParams; +pub use protocol::ExecResponse; +pub use protocol::InitializeParams; +pub use protocol::InitializeResponse; +pub use protocol::ReadParams; +pub use protocol::ReadResponse; +pub use protocol::TerminateParams; +pub use protocol::TerminateResponse; +pub use protocol::WriteParams; +pub use protocol::WriteResponse; +pub use protocol::WriteStatus; +pub use server::DEFAULT_LISTEN_URL; +pub use server::ExecServerListenUrlParseError; +pub use server::run_main; +pub use server::run_main_with_listen_url; diff --git a/codex-rs/exec-server/src/wasm.rs b/codex-rs/exec-server/src/wasm.rs new file mode 100644 index 0000000000..85444a7e2f --- /dev/null +++ b/codex-rs/exec-server/src/wasm.rs @@ -0,0 +1,415 @@ +#[path = "file_system.rs"] +mod file_system; +#[path = "process_id.rs"] +mod process_id; +#[path = "protocol.rs"] +mod protocol; + +use std::io; +use std::sync::Arc; + +use async_trait::async_trait; +use tokio::sync::OnceCell; +use tokio::sync::watch; + +pub use codex_app_server_protocol::FsCopyParams; +pub use codex_app_server_protocol::FsCopyResponse; +pub use codex_app_server_protocol::FsCreateDirectoryParams; +pub use codex_app_server_protocol::FsCreateDirectoryResponse; +pub use codex_app_server_protocol::FsGetMetadataParams; +pub use codex_app_server_protocol::FsGetMetadataResponse; +pub use codex_app_server_protocol::FsReadDirectoryParams; +pub use codex_app_server_protocol::FsReadDirectoryResponse; +pub use codex_app_server_protocol::FsReadFileParams; +pub use codex_app_server_protocol::FsReadFileResponse; +pub use codex_app_server_protocol::FsRemoveParams; +pub use codex_app_server_protocol::FsRemoveResponse; +pub use codex_app_server_protocol::FsWriteFileParams; +pub use codex_app_server_protocol::FsWriteFileResponse; +pub use file_system::CopyOptions; +pub use file_system::CreateDirectoryOptions; +pub use file_system::ExecutorFileSystem; +pub use file_system::FileMetadata; +pub use file_system::FileSystemResult; +pub use file_system::ReadDirectoryEntry; +pub use file_system::RemoveOptions; +pub use process_id::ProcessId; +pub use protocol::ExecClosedNotification; +pub use protocol::ExecExitedNotification; +pub use protocol::ExecOutputDeltaNotification; +pub use protocol::ExecOutputStream; +pub use protocol::ExecParams; +pub use protocol::ExecResponse; +pub use protocol::InitializeParams; +pub use protocol::InitializeResponse; +pub use protocol::ReadParams; +pub use protocol::ReadResponse; +pub use protocol::TerminateParams; +pub use protocol::TerminateResponse; +pub use protocol::WriteParams; +pub use protocol::WriteResponse; +pub use protocol::WriteStatus; + +pub struct StartedExecProcess { + pub process: Arc, +} + +#[async_trait] +pub trait ExecProcess: Send + Sync { + fn process_id(&self) -> &ProcessId; + + fn subscribe_wake(&self) -> watch::Receiver; + + async fn read( + &self, + after_seq: Option, + max_bytes: Option, + wait_ms: Option, + ) -> Result; + + async fn write(&self, chunk: Vec) -> Result; + + async fn terminate(&self) -> Result<(), ExecServerError>; +} + +#[async_trait] +pub trait ExecBackend: Send + Sync { + async fn start(&self, params: ExecParams) -> Result; +} + +pub const CODEX_EXEC_SERVER_URL_ENV_VAR: &str = "CODEX_EXEC_SERVER_URL"; +pub const DEFAULT_LISTEN_URL: &str = "ws://127.0.0.1:0"; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ExecServerClientConnectOptions { + pub client_name: String, + pub initialize_timeout: std::time::Duration, +} + +impl Default for ExecServerClientConnectOptions { + fn default() -> Self { + Self { + client_name: "codex-core".to_string(), + initialize_timeout: std::time::Duration::from_secs(10), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RemoteExecServerConnectArgs { + pub websocket_url: String, + pub client_name: String, + pub connect_timeout: std::time::Duration, + pub initialize_timeout: std::time::Duration, +} + +impl RemoteExecServerConnectArgs { + pub fn new(websocket_url: String, client_name: String) -> Self { + Self { + websocket_url, + client_name, + connect_timeout: std::time::Duration::from_secs(10), + initialize_timeout: std::time::Duration::from_secs(10), + } + } +} + +impl From for ExecServerClientConnectOptions { + fn from(value: RemoteExecServerConnectArgs) -> Self { + Self { + client_name: value.client_name, + initialize_timeout: value.initialize_timeout, + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum ExecServerError { + #[error("exec-server is unavailable on wasm32")] + Unsupported, + #[error("exec-server transport closed")] + Closed, + #[error("failed to serialize or deserialize exec-server JSON: {0}")] + Json(#[from] serde_json::Error), + #[error("exec-server protocol error: {0}")] + Protocol(String), + #[error("exec-server rejected request ({code}): {message}")] + Server { code: i64, message: String }, +} + +#[derive(Debug, thiserror::Error)] +pub enum ExecServerListenUrlParseError { + #[error("exec-server listen URLs are unavailable on wasm32: {0}")] + UnsupportedListenUrl(String), +} + +#[derive(Clone, Default)] +pub struct ExecServerClient; + +impl ExecServerClient { + pub async fn connect_websocket( + _args: RemoteExecServerConnectArgs, + ) -> Result { + Err(ExecServerError::Unsupported) + } + + pub async fn initialize( + &self, + _options: ExecServerClientConnectOptions, + ) -> Result { + Err(ExecServerError::Unsupported) + } + + pub async fn exec(&self, _params: ExecParams) -> Result { + Err(ExecServerError::Unsupported) + } + + pub async fn read(&self, _params: ReadParams) -> Result { + Err(ExecServerError::Unsupported) + } + + pub async fn write( + &self, + _process_id: &ProcessId, + _chunk: Vec, + ) -> Result { + Err(ExecServerError::Unsupported) + } + + pub async fn terminate( + &self, + _process_id: &ProcessId, + ) -> Result { + Err(ExecServerError::Unsupported) + } +} + +pub trait ExecutorEnvironment: Send + Sync { + fn get_exec_backend(&self) -> Arc; +} + +#[derive(Default)] +pub struct EnvironmentManager { + exec_server_url: Option, + current_environment: OnceCell>, +} + +impl EnvironmentManager { + pub fn new(exec_server_url: Option) -> Self { + Self { + exec_server_url: normalize_exec_server_url(exec_server_url), + current_environment: OnceCell::new(), + } + } + + pub fn from_env() -> Self { + Self::new(std::env::var(CODEX_EXEC_SERVER_URL_ENV_VAR).ok()) + } + + pub fn exec_server_url(&self) -> Option<&str> { + self.exec_server_url.as_deref() + } + + pub async fn current(&self) -> Result, ExecServerError> { + self.current_environment + .get_or_init(|| async { Arc::new(Environment::default()) }) + .await; + self.current_environment + .get() + .cloned() + .ok_or(ExecServerError::Unsupported) + } +} + +#[derive(Clone)] +pub struct Environment { + exec_server_url: Option, + exec_backend: Arc, + filesystem: Arc, +} + +impl Default for Environment { + fn default() -> Self { + Self { + exec_server_url: None, + exec_backend: Arc::new(NoopExecBackend), + filesystem: Arc::new(UnsupportedFileSystem), + } + } +} + +impl Environment { + pub async fn create(exec_server_url: Option) -> Result { + Ok(Self { + exec_server_url: normalize_exec_server_url(exec_server_url), + ..Self::default() + }) + } + + pub fn exec_server_url(&self) -> Option<&str> { + self.exec_server_url.as_deref() + } + + pub fn get_exec_backend(&self) -> Arc { + Arc::clone(&self.exec_backend) + } + + pub fn get_filesystem(&self) -> Arc { + Arc::clone(&self.filesystem) + } +} + +impl ExecutorEnvironment for Environment { + fn get_exec_backend(&self) -> Arc { + Arc::clone(&self.exec_backend) + } +} + +#[derive(Debug, Default)] +struct NoopExecBackend; + +#[async_trait] +impl ExecBackend for NoopExecBackend { + async fn start(&self, params: ExecParams) -> Result { + Ok(StartedExecProcess { + process: Arc::new(NoopExecProcess::new(params.process_id)), + }) + } +} + +#[derive(Debug)] +struct NoopExecProcess { + process_id: ProcessId, + wake_tx: watch::Sender, +} + +impl NoopExecProcess { + fn new(process_id: ProcessId) -> Self { + let (wake_tx, _wake_rx) = watch::channel(0); + Self { + process_id, + wake_tx, + } + } +} + +#[async_trait] +impl ExecProcess for NoopExecProcess { + fn process_id(&self) -> &ProcessId { + &self.process_id + } + + fn subscribe_wake(&self) -> watch::Receiver { + self.wake_tx.subscribe() + } + + async fn read( + &self, + _after_seq: Option, + _max_bytes: Option, + _wait_ms: Option, + ) -> Result { + Ok(ReadResponse { + chunks: Vec::new(), + next_seq: 0, + exited: true, + exit_code: Some(1), + closed: true, + failure: Some("exec-server is unavailable on wasm32".to_string()), + }) + } + + async fn write(&self, _chunk: Vec) -> Result { + Ok(WriteResponse { + status: WriteStatus::UnknownProcess, + }) + } + + async fn terminate(&self) -> Result<(), ExecServerError> { + Ok(()) + } +} + +#[derive(Debug, Default)] +struct UnsupportedFileSystem; + +#[async_trait] +impl ExecutorFileSystem for UnsupportedFileSystem { + async fn read_file( + &self, + _path: &codex_utils_absolute_path::AbsolutePathBuf, + ) -> FileSystemResult> { + Err(unsupported_io_error()) + } + + async fn write_file( + &self, + _path: &codex_utils_absolute_path::AbsolutePathBuf, + _contents: Vec, + ) -> FileSystemResult<()> { + Err(unsupported_io_error()) + } + + async fn create_directory( + &self, + _path: &codex_utils_absolute_path::AbsolutePathBuf, + _options: CreateDirectoryOptions, + ) -> FileSystemResult<()> { + Err(unsupported_io_error()) + } + + async fn get_metadata( + &self, + _path: &codex_utils_absolute_path::AbsolutePathBuf, + ) -> FileSystemResult { + Err(unsupported_io_error()) + } + + async fn read_directory( + &self, + _path: &codex_utils_absolute_path::AbsolutePathBuf, + ) -> FileSystemResult> { + Err(unsupported_io_error()) + } + + async fn remove( + &self, + _path: &codex_utils_absolute_path::AbsolutePathBuf, + _options: RemoveOptions, + ) -> FileSystemResult<()> { + Err(unsupported_io_error()) + } + + async fn copy( + &self, + _source_path: &codex_utils_absolute_path::AbsolutePathBuf, + _destination_path: &codex_utils_absolute_path::AbsolutePathBuf, + _options: CopyOptions, + ) -> FileSystemResult<()> { + Err(unsupported_io_error()) + } +} + +pub async fn run_main() -> Result<(), Box> { + Err(Box::new(ExecServerError::Unsupported)) +} + +pub async fn run_main_with_listen_url( + _listen_url: &str, +) -> Result<(), Box> { + Err(Box::new(ExecServerError::Unsupported)) +} + +fn normalize_exec_server_url(exec_server_url: Option) -> Option { + exec_server_url.and_then(|url| { + let url = url.trim(); + (!url.is_empty()).then(|| url.to_string()) + }) +} + +fn unsupported_io_error() -> io::Error { + io::Error::new( + io::ErrorKind::Unsupported, + "exec-server filesystem is unavailable on wasm32", + ) +} diff --git a/codex-rs/network-proxy/Cargo.toml b/codex-rs/network-proxy/Cargo.toml index 6ef6c690ad..e524f3f979 100644 --- a/codex-rs/network-proxy/Cargo.toml +++ b/codex-rs/network-proxy/Cargo.toml @@ -18,15 +18,17 @@ clap = { workspace = true, features = ["derive"] } chrono = { workspace = true } codex-utils-absolute-path = { workspace = true } codex-utils-home-dir = { workspace = true } -codex-utils-rustls-provider = { workspace = true } globset = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } thiserror = { workspace = true } -time = { workspace = true } -tokio = { workspace = true, features = ["full"] } tracing = { workspace = true } url = { workspace = true } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +codex-utils-rustls-provider = { workspace = true } +time = { workspace = true } +tokio = { workspace = true, features = ["full"] } rama-core = { version = "=0.3.0-alpha.4" } rama-http = { version = "=0.3.0-alpha.4" } rama-http-backend = { version = "=0.3.0-alpha.4", features = ["tls"] } @@ -35,9 +37,12 @@ rama-socks5 = { version = "=0.3.0-alpha.4" } rama-tcp = { version = "=0.3.0-alpha.4", features = ["http"] } rama-tls-rustls = { version = "=0.3.0-alpha.4", features = ["http"] } +[target.'cfg(target_arch = "wasm32")'.dependencies] +tokio = { workspace = true, features = ["macros", "rt", "sync"] } + [dev-dependencies] pretty_assertions = { workspace = true } tempfile = { workspace = true } -[target.'cfg(target_family = "unix")'.dependencies] +[target.'cfg(all(target_family = "unix", not(target_arch = "wasm32")))'.dependencies] rama-unix = { version = "=0.3.0-alpha.4" } diff --git a/codex-rs/network-proxy/src/lib.rs b/codex-rs/network-proxy/src/lib.rs index 4fb313805c..12c2a45b59 100644 --- a/codex-rs/network-proxy/src/lib.rs +++ b/codex-rs/network-proxy/src/lib.rs @@ -1,56 +1,9 @@ #![deny(clippy::print_stdout, clippy::print_stderr)] -mod certs; -mod config; -mod http_proxy; -mod mitm; -mod network_policy; -mod policy; -mod proxy; -mod reasons; -mod responses; -mod runtime; -mod socks5; -mod state; -mod upstream; +#[cfg(not(target_arch = "wasm32"))] +include!("native.rs"); -pub use config::NetworkDomainPermission; -pub use config::NetworkDomainPermissionEntry; -pub use config::NetworkDomainPermissions; -pub use config::NetworkMode; -pub use config::NetworkProxyConfig; -pub use config::NetworkUnixSocketPermission; -pub use config::NetworkUnixSocketPermissions; -pub use config::host_and_port_from_network_addr; -pub use network_policy::NetworkDecision; -pub use network_policy::NetworkDecisionSource; -pub use network_policy::NetworkPolicyDecider; -pub use network_policy::NetworkPolicyDecision; -pub use network_policy::NetworkPolicyRequest; -pub use network_policy::NetworkPolicyRequestArgs; -pub use network_policy::NetworkProtocol; -pub use policy::normalize_host; -pub use proxy::ALL_PROXY_ENV_KEYS; -pub use proxy::ALLOW_LOCAL_BINDING_ENV_KEY; -pub use proxy::Args; -pub use proxy::DEFAULT_NO_PROXY_VALUE; -pub use proxy::NO_PROXY_ENV_KEYS; -pub use proxy::NetworkProxy; -pub use proxy::NetworkProxyBuilder; -pub use proxy::NetworkProxyHandle; -pub use proxy::PROXY_URL_ENV_KEYS; -pub use proxy::has_proxy_url_env_vars; -pub use proxy::proxy_url_env_value; -pub use runtime::BlockedRequest; -pub use runtime::BlockedRequestArgs; -pub use runtime::BlockedRequestObserver; -pub use runtime::ConfigReloader; -pub use runtime::ConfigState; -pub use runtime::NetworkProxyState; -pub use state::NetworkProxyAuditMetadata; -pub use state::NetworkProxyConstraintError; -pub use state::NetworkProxyConstraints; -pub use state::PartialNetworkConfig; -pub use state::PartialNetworkProxyConfig; -pub use state::build_config_state; -pub use state::validate_policy_against_constraints; +#[cfg(target_arch = "wasm32")] +mod wasm; +#[cfg(target_arch = "wasm32")] +pub use wasm::*; diff --git a/codex-rs/network-proxy/src/native.rs b/codex-rs/network-proxy/src/native.rs new file mode 100644 index 0000000000..5c2bdb45db --- /dev/null +++ b/codex-rs/network-proxy/src/native.rs @@ -0,0 +1,54 @@ +mod certs; +mod config; +mod http_proxy; +mod mitm; +mod network_policy; +mod policy; +mod proxy; +mod reasons; +mod responses; +mod runtime; +mod socks5; +mod state; +mod upstream; + +pub use config::NetworkDomainPermission; +pub use config::NetworkDomainPermissionEntry; +pub use config::NetworkDomainPermissions; +pub use config::NetworkMode; +pub use config::NetworkProxyConfig; +pub use config::NetworkUnixSocketPermission; +pub use config::NetworkUnixSocketPermissions; +pub use config::host_and_port_from_network_addr; +pub use network_policy::NetworkDecision; +pub use network_policy::NetworkDecisionSource; +pub use network_policy::NetworkPolicyDecider; +pub use network_policy::NetworkPolicyDecision; +pub use network_policy::NetworkPolicyRequest; +pub use network_policy::NetworkPolicyRequestArgs; +pub use network_policy::NetworkProtocol; +pub use policy::normalize_host; +pub use proxy::ALL_PROXY_ENV_KEYS; +pub use proxy::ALLOW_LOCAL_BINDING_ENV_KEY; +pub use proxy::Args; +pub use proxy::DEFAULT_NO_PROXY_VALUE; +pub use proxy::NO_PROXY_ENV_KEYS; +pub use proxy::NetworkProxy; +pub use proxy::NetworkProxyBuilder; +pub use proxy::NetworkProxyHandle; +pub use proxy::PROXY_URL_ENV_KEYS; +pub use proxy::has_proxy_url_env_vars; +pub use proxy::proxy_url_env_value; +pub use runtime::BlockedRequest; +pub use runtime::BlockedRequestArgs; +pub use runtime::BlockedRequestObserver; +pub use runtime::ConfigReloader; +pub use runtime::ConfigState; +pub use runtime::NetworkProxyState; +pub use state::NetworkProxyAuditMetadata; +pub use state::NetworkProxyConstraintError; +pub use state::NetworkProxyConstraints; +pub use state::PartialNetworkConfig; +pub use state::PartialNetworkProxyConfig; +pub use state::build_config_state; +pub use state::validate_policy_against_constraints; diff --git a/codex-rs/network-proxy/src/wasm.rs b/codex-rs/network-proxy/src/wasm.rs new file mode 100644 index 0000000000..720def0168 --- /dev/null +++ b/codex-rs/network-proxy/src/wasm.rs @@ -0,0 +1,863 @@ +#[path = "config.rs"] +mod config; + +use anyhow::Context; +use anyhow::Result; +use async_trait::async_trait; +use serde::Deserialize; +use serde::Serialize; +use std::collections::HashMap; +use std::collections::VecDeque; +use std::future::Future; +use std::net::SocketAddr; +use std::sync::Arc; +use tokio::sync::RwLock; + +pub use config::NetworkDomainPermission; +pub use config::NetworkDomainPermissionEntry; +pub use config::NetworkDomainPermissions; +pub use config::NetworkMode; +pub use config::NetworkProxyConfig; +pub use config::NetworkUnixSocketPermission; +pub use config::NetworkUnixSocketPermissions; +pub use config::host_and_port_from_network_addr; + +pub const PROXY_URL_ENV_KEYS: &[&str] = &[ + "HTTP_PROXY", + "HTTPS_PROXY", + "WS_PROXY", + "WSS_PROXY", + "ALL_PROXY", + "FTP_PROXY", + "YARN_HTTP_PROXY", + "YARN_HTTPS_PROXY", + "NPM_CONFIG_HTTP_PROXY", + "NPM_CONFIG_HTTPS_PROXY", + "NPM_CONFIG_PROXY", + "BUNDLE_HTTP_PROXY", + "BUNDLE_HTTPS_PROXY", + "PIP_PROXY", + "DOCKER_HTTP_PROXY", + "DOCKER_HTTPS_PROXY", +]; +pub const ALL_PROXY_ENV_KEYS: &[&str] = &["ALL_PROXY", "all_proxy"]; +pub const ALLOW_LOCAL_BINDING_ENV_KEY: &str = "CODEX_NETWORK_ALLOW_LOCAL_BINDING"; +pub const NO_PROXY_ENV_KEYS: &[&str] = &[ + "NO_PROXY", + "no_proxy", + "npm_config_noproxy", + "NPM_CONFIG_NOPROXY", + "YARN_NO_PROXY", + "BUNDLE_NO_PROXY", +]; +pub const DEFAULT_NO_PROXY_VALUE: &str = concat!( + "localhost,127.0.0.1,::1,", + "*.local,.local,", + "169.254.0.0/16,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16" +); + +const FTP_PROXY_ENV_KEYS: &[&str] = &["FTP_PROXY", "ftp_proxy"]; +const WEBSOCKET_PROXY_ENV_KEYS: &[&str] = &["WS_PROXY", "WSS_PROXY", "ws_proxy", "wss_proxy"]; + +#[derive(Debug, Clone, clap::Parser)] +#[command(name = "codex-network-proxy", about = "Codex network sandbox proxy")] +pub struct Args {} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum NetworkProtocol { + Http, + HttpsConnect, + Socks5Tcp, + Socks5Udp, +} + +impl NetworkProtocol { + pub const fn as_policy_protocol(self) -> &'static str { + match self { + Self::Http => "http", + Self::HttpsConnect => "https_connect", + Self::Socks5Tcp => "socks5_tcp", + Self::Socks5Udp => "socks5_udp", + } + } +} + +#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum NetworkPolicyDecision { + Deny, + Ask, +} + +impl NetworkPolicyDecision { + pub const fn as_str(self) -> &'static str { + match self { + Self::Deny => "deny", + Self::Ask => "ask", + } + } +} + +#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum NetworkDecisionSource { + BaselinePolicy, + ModeGuard, + ProxyState, + Decider, +} + +impl NetworkDecisionSource { + pub const fn as_str(self) -> &'static str { + match self { + Self::BaselinePolicy => "baseline_policy", + Self::ModeGuard => "mode_guard", + Self::ProxyState => "proxy_state", + Self::Decider => "decider", + } + } +} + +#[derive(Clone, Debug)] +pub struct NetworkPolicyRequest { + pub protocol: NetworkProtocol, + pub host: String, + pub port: u16, + pub client_addr: Option, + pub method: Option, + pub command: Option, + pub exec_policy_hint: Option, +} + +pub struct NetworkPolicyRequestArgs { + pub protocol: NetworkProtocol, + pub host: String, + pub port: u16, + pub client_addr: Option, + pub method: Option, + pub command: Option, + pub exec_policy_hint: Option, +} + +impl NetworkPolicyRequest { + pub fn new(args: NetworkPolicyRequestArgs) -> Self { + let NetworkPolicyRequestArgs { + protocol, + host, + port, + client_addr, + method, + command, + exec_policy_hint, + } = args; + Self { + protocol, + host, + port, + client_addr, + method, + command, + exec_policy_hint, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum NetworkDecision { + Allow, + Deny { + reason: String, + source: NetworkDecisionSource, + decision: NetworkPolicyDecision, + }, +} + +impl NetworkDecision { + pub fn deny(reason: impl Into) -> Self { + Self::deny_with_source(reason, NetworkDecisionSource::Decider) + } + + pub fn ask(reason: impl Into) -> Self { + Self::ask_with_source(reason, NetworkDecisionSource::Decider) + } + + pub fn deny_with_source(reason: impl Into, source: NetworkDecisionSource) -> Self { + Self::Deny { + reason: reason.into(), + source, + decision: NetworkPolicyDecision::Deny, + } + } + + pub fn ask_with_source(reason: impl Into, source: NetworkDecisionSource) -> Self { + Self::Deny { + reason: reason.into(), + source, + decision: NetworkPolicyDecision::Ask, + } + } +} + +#[async_trait] +pub trait NetworkPolicyDecider: Send + Sync + 'static { + async fn decide(&self, request: NetworkPolicyRequest) -> Result; +} + +#[async_trait] +impl NetworkPolicyDecider for Arc { + async fn decide(&self, request: NetworkPolicyRequest) -> Result { + (**self).decide(request).await + } +} + +#[async_trait] +impl NetworkPolicyDecider for F +where + F: Fn(NetworkPolicyRequest) -> Fut + Send + Sync + 'static, + Fut: Future> + Send, +{ + async fn decide(&self, request: NetworkPolicyRequest) -> Result { + (self)(request).await + } +} + +pub fn normalize_host(host: &str) -> String { + let host = host.trim(); + if host.starts_with('[') + && let Some(end) = host.find(']') + { + return host[1..end] + .to_ascii_lowercase() + .trim_end_matches('.') + .to_string(); + } + + if host.bytes().filter(|b| *b == b':').count() == 1 { + let trimmed = host.split(':').next().unwrap_or_default(); + return trimmed + .to_ascii_lowercase() + .trim_end_matches('.') + .to_string(); + } + + host.to_ascii_lowercase().trim_end_matches('.').to_string() +} + +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct NetworkProxyConstraints { + pub enabled: Option, + pub mode: Option, + pub allow_upstream_proxy: Option, + pub dangerously_allow_non_loopback_proxy: Option, + pub dangerously_allow_all_unix_sockets: Option, + pub allowed_domains: Option>, + pub allowlist_expansion_enabled: Option, + pub denied_domains: Option>, + pub denylist_expansion_enabled: Option, + pub allow_unix_sockets: Option>, + pub allow_local_binding: Option, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct PartialNetworkProxyConfig { + #[serde(default)] + pub network: PartialNetworkConfig, +} + +#[derive(Debug, Default, Clone, Deserialize)] +pub struct PartialNetworkConfig { + pub enabled: Option, + pub mode: Option, + pub allow_upstream_proxy: Option, + pub dangerously_allow_non_loopback_proxy: Option, + pub dangerously_allow_all_unix_sockets: Option, + #[serde(default)] + pub domains: Option, + #[serde(default)] + pub unix_sockets: Option, + pub allow_local_binding: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +pub enum NetworkProxyConstraintError { + #[error("invalid value for {field_name}: {candidate} (allowed {allowed})")] + InvalidValue { + field_name: &'static str, + candidate: String, + allowed: String, + }, +} + +impl NetworkProxyConstraintError { + pub fn into_anyhow(self) -> anyhow::Error { + anyhow::anyhow!(self) + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct NetworkProxyAuditMetadata { + pub conversation_id: Option, + pub app_version: Option, + pub user_account_id: Option, + pub auth_mode: Option, + pub originator: Option, + pub user_email: Option, + pub terminal_type: Option, + pub model: Option, + pub slug: Option, +} + +#[derive(Clone, Debug, serde::Serialize)] +pub struct BlockedRequest { + pub host: String, + pub reason: String, + pub client: Option, + pub method: Option, + pub mode: Option, + pub protocol: String, + pub decision: Option, + pub source: Option, + pub port: Option, + pub timestamp: i64, +} + +pub struct BlockedRequestArgs { + pub host: String, + pub reason: String, + pub client: Option, + pub method: Option, + pub mode: Option, + pub protocol: String, + pub decision: Option, + pub source: Option, + pub port: Option, +} + +impl BlockedRequest { + pub fn new(args: BlockedRequestArgs) -> Self { + let BlockedRequestArgs { + host, + reason, + client, + method, + mode, + protocol, + decision, + source, + port, + } = args; + Self { + host, + reason, + client, + method, + mode, + protocol, + decision, + source, + port, + timestamp: chrono::Utc::now().timestamp(), + } + } +} + +#[derive(Clone)] +pub struct ConfigState { + pub config: NetworkProxyConfig, + pub constraints: NetworkProxyConstraints, + pub blocked: VecDeque, + pub blocked_total: u64, +} + +#[async_trait] +pub trait ConfigReloader: Send + Sync { + fn source_label(&self) -> String; + async fn maybe_reload(&self) -> Result>; + async fn reload_now(&self) -> Result; +} + +#[async_trait] +pub trait BlockedRequestObserver: Send + Sync + 'static { + async fn on_blocked_request(&self, request: BlockedRequest); +} + +#[async_trait] +impl BlockedRequestObserver for Arc { + async fn on_blocked_request(&self, request: BlockedRequest) { + (**self).on_blocked_request(request).await + } +} + +#[async_trait] +impl BlockedRequestObserver for F +where + F: Fn(BlockedRequest) -> Fut + Send + Sync + 'static, + Fut: Future + Send, +{ + async fn on_blocked_request(&self, request: BlockedRequest) { + (self)(request).await; + } +} + +pub fn build_config_state( + config: NetworkProxyConfig, + constraints: NetworkProxyConstraints, +) -> anyhow::Result { + validate_policy_against_constraints(&config, &constraints)?; + Ok(ConfigState { + config, + constraints, + blocked: VecDeque::new(), + blocked_total: 0, + }) +} + +pub fn validate_policy_against_constraints( + config: &NetworkProxyConfig, + constraints: &NetworkProxyConstraints, +) -> Result<(), NetworkProxyConstraintError> { + if let Some(false) = constraints.enabled + && config.network.enabled + { + return Err(NetworkProxyConstraintError::InvalidValue { + field_name: "network.enabled", + candidate: "true".to_string(), + allowed: "false (disabled by managed config)".to_string(), + }); + } + + if let Some(false) = constraints.allow_local_binding + && config.network.allow_local_binding + { + return Err(NetworkProxyConstraintError::InvalidValue { + field_name: "network.allow_local_binding", + candidate: "true".to_string(), + allowed: "false (disabled by managed config)".to_string(), + }); + } + + Ok(()) +} + +pub struct NetworkProxyState { + state: Arc>, + reloader: Arc, + blocked_request_observer: Arc>>>, + audit_metadata: NetworkProxyAuditMetadata, +} + +impl std::fmt::Debug for NetworkProxyState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("NetworkProxyState").finish_non_exhaustive() + } +} + +impl Clone for NetworkProxyState { + fn clone(&self) -> Self { + Self { + state: self.state.clone(), + reloader: self.reloader.clone(), + blocked_request_observer: self.blocked_request_observer.clone(), + audit_metadata: self.audit_metadata.clone(), + } + } +} + +impl NetworkProxyState { + pub fn with_reloader(state: ConfigState, reloader: Arc) -> Self { + Self::with_reloader_and_audit_metadata( + state, + reloader, + NetworkProxyAuditMetadata::default(), + ) + } + + pub fn with_reloader_and_blocked_observer( + state: ConfigState, + reloader: Arc, + blocked_request_observer: Option>, + ) -> Self { + Self::with_reloader_and_audit_metadata_and_blocked_observer( + state, + reloader, + NetworkProxyAuditMetadata::default(), + blocked_request_observer, + ) + } + + pub fn with_reloader_and_audit_metadata( + state: ConfigState, + reloader: Arc, + audit_metadata: NetworkProxyAuditMetadata, + ) -> Self { + Self::with_reloader_and_audit_metadata_and_blocked_observer( + state, + reloader, + audit_metadata, + None, + ) + } + + pub fn with_reloader_and_audit_metadata_and_blocked_observer( + state: ConfigState, + reloader: Arc, + audit_metadata: NetworkProxyAuditMetadata, + blocked_request_observer: Option>, + ) -> Self { + Self { + state: Arc::new(RwLock::new(state)), + reloader, + blocked_request_observer: Arc::new(RwLock::new(blocked_request_observer)), + audit_metadata, + } + } + + pub async fn set_blocked_request_observer( + &self, + blocked_request_observer: Option>, + ) { + let mut observer = self.blocked_request_observer.write().await; + *observer = blocked_request_observer; + } + + pub fn audit_metadata(&self) -> &NetworkProxyAuditMetadata { + &self.audit_metadata + } + + pub async fn current_cfg(&self) -> Result { + self.reload_if_needed().await?; + let guard = self.state.read().await; + Ok(guard.config.clone()) + } + + pub async fn add_allowed_domain(&self, host: &str) -> Result<()> { + let mut guard = self.state.write().await; + guard.config.network.upsert_domain_permission( + host.to_string(), + NetworkDomainPermission::Allow, + normalize_host, + ); + Ok(()) + } + + pub async fn add_denied_domain(&self, host: &str) -> Result<()> { + let mut guard = self.state.write().await; + guard.config.network.upsert_domain_permission( + host.to_string(), + NetworkDomainPermission::Deny, + normalize_host, + ); + Ok(()) + } + + pub async fn record_blocked(&self, entry: BlockedRequest) -> Result<()> { + let blocked_for_observer = entry.clone(); + let blocked_request_observer = self.blocked_request_observer.read().await.clone(); + let mut guard = self.state.write().await; + guard.blocked.push_back(entry); + guard.blocked_total = guard.blocked_total.saturating_add(1); + while guard.blocked.len() > 200 { + guard.blocked.pop_front(); + } + drop(guard); + + if let Some(observer) = blocked_request_observer { + observer.on_blocked_request(blocked_for_observer).await; + } + Ok(()) + } + + async fn reload_if_needed(&self) -> Result<()> { + if let Some(mut new_state) = self.reloader.maybe_reload().await? { + let blocked = { + let guard = self.state.read().await; + (guard.blocked.clone(), guard.blocked_total) + }; + new_state.blocked = blocked.0; + new_state.blocked_total = blocked.1; + let mut guard = self.state.write().await; + *guard = new_state; + } + Ok(()) + } +} + +#[derive(Clone)] +pub struct NetworkProxyBuilder { + state: Option>, + http_addr: Option, + socks_addr: Option, + _managed_by_codex: bool, + policy_decider: Option>, + blocked_request_observer: Option>, +} + +impl Default for NetworkProxyBuilder { + fn default() -> Self { + Self { + state: None, + http_addr: None, + socks_addr: None, + _managed_by_codex: true, + policy_decider: None, + blocked_request_observer: None, + } + } +} + +impl NetworkProxyBuilder { + pub fn state(mut self, state: Arc) -> Self { + self.state = Some(state); + self + } + + pub fn http_addr(mut self, addr: SocketAddr) -> Self { + self.http_addr = Some(addr); + self + } + + pub fn socks_addr(mut self, addr: SocketAddr) -> Self { + self.socks_addr = Some(addr); + self + } + + pub fn managed_by_codex(mut self, managed_by_codex: bool) -> Self { + self._managed_by_codex = managed_by_codex; + self + } + + pub fn policy_decider(mut self, decider: D) -> Self + where + D: NetworkPolicyDecider, + { + self.policy_decider = Some(Arc::new(decider)); + self + } + + pub fn policy_decider_arc(mut self, decider: Arc) -> Self { + self.policy_decider = Some(decider); + self + } + + pub fn blocked_request_observer(mut self, observer: O) -> Self + where + O: BlockedRequestObserver, + { + self.blocked_request_observer = Some(Arc::new(observer)); + self + } + + pub fn blocked_request_observer_arc( + mut self, + observer: Arc, + ) -> Self { + self.blocked_request_observer = Some(observer); + self + } + + pub async fn build(self) -> Result { + let state = self.state.context("NetworkProxyBuilder requires a state")?; + state + .set_blocked_request_observer(self.blocked_request_observer.clone()) + .await; + let current_cfg = state.current_cfg().await?; + let http_addr = self + .http_addr + .unwrap_or(SocketAddr::from(([127, 0, 0, 1], 0))); + let socks_addr = self + .socks_addr + .unwrap_or(SocketAddr::from(([127, 0, 0, 1], 0))); + Ok(NetworkProxy { + state, + http_addr, + socks_addr, + socks_enabled: current_cfg.network.enable_socks5, + allow_local_binding: current_cfg.network.allow_local_binding, + allow_unix_sockets: current_cfg.network.allow_unix_sockets(), + dangerously_allow_all_unix_sockets: current_cfg + .network + .dangerously_allow_all_unix_sockets, + _policy_decider: self.policy_decider, + }) + } +} + +#[derive(Clone)] +pub struct NetworkProxy { + state: Arc, + http_addr: SocketAddr, + socks_addr: SocketAddr, + socks_enabled: bool, + allow_local_binding: bool, + allow_unix_sockets: Vec, + dangerously_allow_all_unix_sockets: bool, + _policy_decider: Option>, +} + +impl std::fmt::Debug for NetworkProxy { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("NetworkProxy") + .field("http_addr", &self.http_addr) + .field("socks_addr", &self.socks_addr) + .finish_non_exhaustive() + } +} + +impl PartialEq for NetworkProxy { + fn eq(&self, other: &Self) -> bool { + self.http_addr == other.http_addr + && self.socks_addr == other.socks_addr + && self.allow_local_binding == other.allow_local_binding + } +} + +impl Eq for NetworkProxy {} + +pub fn proxy_url_env_value<'a>( + env: &'a HashMap, + canonical_key: &str, +) -> Option<&'a str> { + if let Some(value) = env.get(canonical_key) { + return Some(value.as_str()); + } + let lower_key = canonical_key.to_ascii_lowercase(); + env.get(lower_key.as_str()).map(String::as_str) +} + +pub fn has_proxy_url_env_vars(env: &HashMap) -> bool { + PROXY_URL_ENV_KEYS + .iter() + .any(|key| proxy_url_env_value(env, key).is_some_and(|value| !value.trim().is_empty())) +} + +fn set_env_keys(env: &mut HashMap, keys: &[&str], value: &str) { + for key in keys { + env.insert((*key).to_string(), value.to_string()); + } +} + +fn apply_proxy_env_overrides( + env: &mut HashMap, + http_addr: SocketAddr, + socks_addr: SocketAddr, + socks_enabled: bool, + allow_local_binding: bool, +) { + let http_proxy_url = format!("http://{http_addr}"); + let socks_proxy_url = format!("socks5h://{socks_addr}"); + env.insert( + ALLOW_LOCAL_BINDING_ENV_KEY.to_string(), + if allow_local_binding { "1" } else { "0" }.to_string(), + ); + set_env_keys( + env, + &[ + "HTTP_PROXY", + "HTTPS_PROXY", + "http_proxy", + "https_proxy", + "YARN_HTTP_PROXY", + "YARN_HTTPS_PROXY", + "npm_config_http_proxy", + "npm_config_https_proxy", + "npm_config_proxy", + "NPM_CONFIG_HTTP_PROXY", + "NPM_CONFIG_HTTPS_PROXY", + "NPM_CONFIG_PROXY", + "BUNDLE_HTTP_PROXY", + "BUNDLE_HTTPS_PROXY", + "PIP_PROXY", + "DOCKER_HTTP_PROXY", + "DOCKER_HTTPS_PROXY", + ], + &http_proxy_url, + ); + set_env_keys(env, WEBSOCKET_PROXY_ENV_KEYS, &http_proxy_url); + set_env_keys(env, NO_PROXY_ENV_KEYS, DEFAULT_NO_PROXY_VALUE); + env.insert("ELECTRON_GET_USE_PROXY".to_string(), "true".to_string()); + if socks_enabled { + set_env_keys(env, ALL_PROXY_ENV_KEYS, &socks_proxy_url); + set_env_keys(env, FTP_PROXY_ENV_KEYS, &socks_proxy_url); + } else { + set_env_keys(env, ALL_PROXY_ENV_KEYS, &http_proxy_url); + set_env_keys(env, FTP_PROXY_ENV_KEYS, &http_proxy_url); + } +} + +impl NetworkProxy { + pub fn builder() -> NetworkProxyBuilder { + NetworkProxyBuilder::default() + } + + pub fn http_addr(&self) -> SocketAddr { + self.http_addr + } + + pub fn socks_addr(&self) -> SocketAddr { + self.socks_addr + } + + pub async fn current_cfg(&self) -> Result { + self.state.current_cfg().await + } + + pub async fn add_allowed_domain(&self, host: &str) -> Result<()> { + self.state.add_allowed_domain(host).await + } + + pub async fn add_denied_domain(&self, host: &str) -> Result<()> { + self.state.add_denied_domain(host).await + } + + pub fn allow_local_binding(&self) -> bool { + self.allow_local_binding + } + + pub fn allow_unix_sockets(&self) -> &[String] { + &self.allow_unix_sockets + } + + pub fn dangerously_allow_all_unix_sockets(&self) -> bool { + self.dangerously_allow_all_unix_sockets + } + + pub fn apply_to_env(&self, env: &mut HashMap) { + apply_proxy_env_overrides( + env, + self.http_addr, + self.socks_addr, + self.socks_enabled, + self.allow_local_binding, + ); + } + + pub async fn run(&self) -> Result { + Ok(NetworkProxyHandle::noop()) + } +} + +pub struct NetworkProxyHandle { + completed: bool, +} + +impl NetworkProxyHandle { + fn noop() -> Self { + Self { completed: true } + } + + pub async fn wait(mut self) -> Result<()> { + self.completed = true; + Ok(()) + } + + pub async fn shutdown(mut self) -> Result<()> { + self.completed = true; + Ok(()) + } +} + +impl Drop for NetworkProxyHandle { + fn drop(&mut self) { + let _ = self.completed; + } +} diff --git a/codex-rs/rmcp-client/Cargo.toml b/codex-rs/rmcp-client/Cargo.toml index 4b20e9d6eb..ef8744922d 100644 --- a/codex-rs/rmcp-client/Cargo.toml +++ b/codex-rs/rmcp-client/Cargo.toml @@ -3,22 +3,54 @@ name = "codex-rmcp-client" version.workspace = true edition.workspace = true license.workspace = true +autobins = false [lints] workspace = true +[features] +native-bin = [] + +[[bin]] +name = "rmcp_test_server" +path = "src/bin/rmcp_test_server.rs" +required-features = ["native-bin"] + +[[bin]] +name = "test_stdio_server" +path = "src/bin/test_stdio_server.rs" +required-features = ["native-bin"] + +[[bin]] +name = "test_streamable_http_server" +path = "src/bin/test_streamable_http_server.rs" +required-features = ["native-bin"] + [dependencies] anyhow = "1" +codex-protocol = { workspace = true } +futures = { workspace = true, default-features = false, features = ["std"] } +schemars = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +thiserror = { workspace = true } +tracing = { workspace = true, features = ["log"] } + +[dev-dependencies] +codex-utils-cargo-bin = { workspace = true } +pretty_assertions = { workspace = true } +serial_test = { workspace = true } +tempfile = { workspace = true } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] axum = { workspace = true, default-features = false, features = [ "http1", "tokio", ] } codex-client = { workspace = true } codex-keyring-store = { workspace = true } -codex-protocol = { workspace = true } -codex-utils-pty = { workspace = true } codex-utils-home-dir = { workspace = true } -futures = { workspace = true, default-features = false, features = ["std"] } +codex-utils-pty = { workspace = true } keyring = { workspace = true, features = ["crypto-rust"] } oauth2 = "5" reqwest = { version = "0.12", default-features = false, features = [ @@ -37,12 +69,8 @@ rmcp = { workspace = true, default-features = false, features = [ "transport-streamable-http-client-reqwest", "transport-streamable-http-server", ] } -schemars = { workspace = true } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } sha2 = { workspace = true } sse-stream = "0.2.1" -thiserror = { workspace = true } tiny_http = { workspace = true } tokio = { workspace = true, features = [ "io-util", @@ -53,24 +81,18 @@ tokio = { workspace = true, features = [ "io-std", "time", ] } -tracing = { workspace = true, features = ["log"] } urlencoding = { workspace = true } webbrowser = { workspace = true } which = { workspace = true } -[dev-dependencies] -codex-utils-cargo-bin = { workspace = true } -pretty_assertions = { workspace = true } -serial_test = { workspace = true } -tempfile = { workspace = true } -[target.'cfg(target_os = "linux")'.dependencies] +[target.'cfg(all(not(target_arch = "wasm32"), target_os = "linux"))'.dependencies] keyring = { workspace = true, features = ["linux-native-async-persistent"] } -[target.'cfg(target_os = "macos")'.dependencies] +[target.'cfg(all(not(target_arch = "wasm32"), target_os = "macos"))'.dependencies] keyring = { workspace = true, features = ["apple-native"] } -[target.'cfg(target_os = "windows")'.dependencies] +[target.'cfg(all(not(target_arch = "wasm32"), target_os = "windows"))'.dependencies] keyring = { workspace = true, features = ["windows-native"] } -[target.'cfg(any(target_os = "freebsd", target_os = "openbsd"))'.dependencies] +[target.'cfg(all(not(target_arch = "wasm32"), any(target_os = "freebsd", target_os = "openbsd")))'.dependencies] keyring = { workspace = true, features = ["sync-secret-service"] } diff --git a/codex-rs/rmcp-client/src/lib.rs b/codex-rs/rmcp-client/src/lib.rs index 627b9f2e71..22186e8715 100644 --- a/codex-rs/rmcp-client/src/lib.rs +++ b/codex-rs/rmcp-client/src/lib.rs @@ -1,31 +1,7 @@ -mod auth_status; -mod logging_client_handler; -mod oauth; -mod perform_oauth_login; -mod program_resolver; -mod rmcp_client; -mod utils; +#[cfg(not(target_arch = "wasm32"))] +include!("native.rs"); -pub use auth_status::StreamableHttpOAuthDiscovery; -pub use auth_status::determine_streamable_http_auth_status; -pub use auth_status::discover_streamable_http_oauth; -pub use auth_status::supports_oauth_login; -pub use codex_protocol::protocol::McpAuthStatus; -pub use oauth::OAuthCredentialsStoreMode; -pub use oauth::StoredOAuthTokens; -pub use oauth::WrappedOAuthTokenResponse; -pub use oauth::delete_oauth_tokens; -pub(crate) use oauth::load_oauth_tokens; -pub use oauth::save_oauth_tokens; -pub use perform_oauth_login::OAuthProviderError; -pub use perform_oauth_login::OauthLoginHandle; -pub use perform_oauth_login::perform_oauth_login; -pub use perform_oauth_login::perform_oauth_login_return_url; -pub use perform_oauth_login::perform_oauth_login_silent; -pub use rmcp::model::ElicitationAction; -pub use rmcp_client::Elicitation; -pub use rmcp_client::ElicitationResponse; -pub use rmcp_client::ListToolsWithConnectorIdResult; -pub use rmcp_client::RmcpClient; -pub use rmcp_client::SendElicitation; -pub use rmcp_client::ToolWithConnectorId; +#[cfg(target_arch = "wasm32")] +mod wasm; +#[cfg(target_arch = "wasm32")] +pub use wasm::*; diff --git a/codex-rs/rmcp-client/src/native.rs b/codex-rs/rmcp-client/src/native.rs new file mode 100644 index 0000000000..627b9f2e71 --- /dev/null +++ b/codex-rs/rmcp-client/src/native.rs @@ -0,0 +1,31 @@ +mod auth_status; +mod logging_client_handler; +mod oauth; +mod perform_oauth_login; +mod program_resolver; +mod rmcp_client; +mod utils; + +pub use auth_status::StreamableHttpOAuthDiscovery; +pub use auth_status::determine_streamable_http_auth_status; +pub use auth_status::discover_streamable_http_oauth; +pub use auth_status::supports_oauth_login; +pub use codex_protocol::protocol::McpAuthStatus; +pub use oauth::OAuthCredentialsStoreMode; +pub use oauth::StoredOAuthTokens; +pub use oauth::WrappedOAuthTokenResponse; +pub use oauth::delete_oauth_tokens; +pub(crate) use oauth::load_oauth_tokens; +pub use oauth::save_oauth_tokens; +pub use perform_oauth_login::OAuthProviderError; +pub use perform_oauth_login::OauthLoginHandle; +pub use perform_oauth_login::perform_oauth_login; +pub use perform_oauth_login::perform_oauth_login_return_url; +pub use perform_oauth_login::perform_oauth_login_silent; +pub use rmcp::model::ElicitationAction; +pub use rmcp_client::Elicitation; +pub use rmcp_client::ElicitationResponse; +pub use rmcp_client::ListToolsWithConnectorIdResult; +pub use rmcp_client::RmcpClient; +pub use rmcp_client::SendElicitation; +pub use rmcp_client::ToolWithConnectorId; diff --git a/codex-rs/rmcp-client/src/wasm.rs b/codex-rs/rmcp-client/src/wasm.rs new file mode 100644 index 0000000000..5a4756f1df --- /dev/null +++ b/codex-rs/rmcp-client/src/wasm.rs @@ -0,0 +1,340 @@ +use std::collections::HashMap; +use std::ffi::OsString; +use std::path::PathBuf; +use std::time::Duration; + +use anyhow::Result; +use anyhow::anyhow; +use futures::future::BoxFuture; +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; + +pub use codex_protocol::protocol::McpAuthStatus; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum ElicitationAction { + Accept, + Decline, + Cancel, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct StreamableHttpOAuthDiscovery { + pub scopes_supported: Option>, +} + +pub async fn determine_streamable_http_auth_status( + _server_name: &str, + _url: &str, + bearer_token_env_var: Option<&str>, + http_headers: Option>, + env_http_headers: Option>, + _store_mode: OAuthCredentialsStoreMode, +) -> Result { + if bearer_token_env_var.is_some() { + return Ok(McpAuthStatus::BearerToken); + } + + let has_auth_header = http_headers + .into_iter() + .flatten() + .chain(env_http_headers.into_iter().flatten()) + .any(|(key, _)| key.eq_ignore_ascii_case("authorization")); + if has_auth_header { + return Ok(McpAuthStatus::BearerToken); + } + + Ok(McpAuthStatus::Unsupported) +} + +pub async fn supports_oauth_login(_url: &str) -> Result { + Ok(false) +} + +pub async fn discover_streamable_http_oauth( + _url: &str, + _http_headers: Option>, + _env_http_headers: Option>, +) -> Result> { + Ok(None) +} + +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "lowercase")] +pub enum OAuthCredentialsStoreMode { + #[default] + Auto, + File, + Keyring, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct StoredOAuthTokens { + pub server_name: String, + pub url: String, + pub client_id: String, + pub token_response: WrappedOAuthTokenResponse, + #[serde(default)] + pub expires_at: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +pub struct WrappedOAuthTokenResponse(pub serde_json::Value); + +pub fn delete_oauth_tokens( + _server_name: &str, + _url: &str, + _store_mode: OAuthCredentialsStoreMode, +) -> Result { + Ok(false) +} + +pub fn save_oauth_tokens( + _server_name: &str, + _tokens: &StoredOAuthTokens, + _store_mode: OAuthCredentialsStoreMode, +) -> Result<()> { + Ok(()) +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct OAuthProviderError { + error: Option, + error_description: Option, +} + +impl OAuthProviderError { + pub fn new(error: Option, error_description: Option) -> Self { + Self { + error, + error_description, + } + } +} + +impl std::fmt::Display for OAuthProviderError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match (self.error.as_deref(), self.error_description.as_deref()) { + (Some(error), Some(error_description)) => { + write!(f, "OAuth provider returned `{error}`: {error_description}") + } + (Some(error), None) => write!(f, "OAuth provider returned `{error}`"), + (None, Some(error_description)) => write!(f, "OAuth error: {error_description}"), + (None, None) => write!(f, "OAuth provider returned an error"), + } + } +} + +impl std::error::Error for OAuthProviderError {} + +#[allow(clippy::too_many_arguments)] +pub async fn perform_oauth_login( + _server_name: &str, + _server_url: &str, + _store_mode: OAuthCredentialsStoreMode, + _http_headers: Option>, + _env_http_headers: Option>, + _scopes: &[String], + _oauth_resource: Option<&str>, + _callback_port: Option, + _callback_url: Option<&str>, +) -> Result<()> { + Err(anyhow!("MCP OAuth login is unavailable on wasm32")) +} + +#[allow(clippy::too_many_arguments)] +pub async fn perform_oauth_login_silent( + _server_name: &str, + _server_url: &str, + _store_mode: OAuthCredentialsStoreMode, + _http_headers: Option>, + _env_http_headers: Option>, + _scopes: &[String], + _oauth_resource: Option<&str>, + _callback_port: Option, + _callback_url: Option<&str>, +) -> Result<()> { + Err(anyhow!("MCP OAuth login is unavailable on wasm32")) +} + +#[allow(clippy::too_many_arguments)] +pub async fn perform_oauth_login_return_url( + _server_name: &str, + _server_url: &str, + _store_mode: OAuthCredentialsStoreMode, + _http_headers: Option>, + _env_http_headers: Option>, + _scopes: &[String], + _oauth_resource: Option<&str>, + _callback_port: Option, + _callback_url: Option<&str>, +) -> Result { + Err(anyhow!("MCP OAuth login is unavailable on wasm32")) +} + +pub struct OauthLoginHandle { + authorization_url: String, +} + +impl OauthLoginHandle { + pub fn authorization_url(&self) -> &str { + &self.authorization_url + } + + pub fn into_parts(self) -> (String, futures::channel::oneshot::Receiver>) { + let (_tx, rx) = futures::channel::oneshot::channel(); + (self.authorization_url, rx) + } + + pub async fn wait(self) -> Result<()> { + Err(anyhow!("MCP OAuth login is unavailable on wasm32")) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] +pub struct RequestId(pub String); + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +pub struct Elicitation { + #[serde(default)] + pub message: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ElicitationResponse { + pub action: ElicitationAction, + pub content: Option, + #[serde(rename = "_meta")] + pub meta: Option, +} + +pub type SendElicitation = Box< + dyn Fn(RequestId, Elicitation) -> BoxFuture<'static, Result> + Send + Sync, +>; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +pub struct ToolDefinition { + #[serde(default)] + pub name: String, +} + +pub struct ToolWithConnectorId { + pub tool: ToolDefinition, + pub connector_id: Option, + pub connector_name: Option, + pub connector_description: Option, +} + +pub struct ListToolsWithConnectorIdResult { + pub next_cursor: Option, + pub tools: Vec, +} + +pub struct RmcpClient; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +pub struct InitializeRequestParams; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +pub struct InitializeResult; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +pub struct PaginatedRequestParams { + #[serde(default)] + pub cursor: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +pub struct ListResourcesResult; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +pub struct ListResourceTemplatesResult; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +pub struct ReadResourceRequestParams; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +pub struct ReadResourceResult; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +pub struct CallToolResult; + +impl RmcpClient { + pub async fn new_stdio_client( + _program: OsString, + _args: Vec, + _env: Option>, + _env_vars: &[String], + _cwd: Option, + ) -> std::io::Result { + Err(std::io::Error::other( + "MCP stdio transport is unavailable on wasm32", + )) + } + + #[allow(clippy::too_many_arguments)] + pub async fn new_streamable_http_client( + _server_name: &str, + _url: &str, + _bearer_token: Option, + _http_headers: Option>, + _env_http_headers: Option>, + _store_mode: OAuthCredentialsStoreMode, + ) -> Result { + Err(anyhow!("MCP HTTP transport is unavailable on wasm32")) + } + + pub async fn initialize( + &self, + _params: InitializeRequestParams, + _timeout: Option, + _send_elicitation: SendElicitation, + ) -> Result { + Err(anyhow!("MCP initialize is unavailable on wasm32")) + } + + pub async fn list_tools_with_connector_ids( + &self, + _params: Option, + _timeout: Option, + ) -> Result { + Err(anyhow!("MCP tools are unavailable on wasm32")) + } + + pub async fn list_resources( + &self, + _params: Option, + _timeout: Option, + ) -> Result { + Err(anyhow!("MCP resources are unavailable on wasm32")) + } + + pub async fn list_resource_templates( + &self, + _params: Option, + _timeout: Option, + ) -> Result { + Err(anyhow!("MCP resource templates are unavailable on wasm32")) + } + + pub async fn read_resource( + &self, + _params: ReadResourceRequestParams, + _timeout: Option, + ) -> Result { + Err(anyhow!("MCP resources are unavailable on wasm32")) + } + + pub async fn call_tool( + &self, + _name: String, + _arguments: Option, + _meta: Option, + _timeout: Option, + ) -> Result { + Err(anyhow!("MCP tools are unavailable on wasm32")) + } +}