diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 1f7934bc45..6ace22c766 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -849,6 +849,7 @@ dependencies = [ "codex-login", "codex-protocol", "codex-utils-json-to-toml", + "codex-windows-sandbox", "core_test_support", "mcp-types", "opentelemetry-appender-tracing", diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index f5408a6225..f5317a2028 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -494,6 +494,9 @@ server_notification_definitions! { ReasoningSummaryPartAdded => "item/reasoning/summaryPartAdded" (v2::ReasoningSummaryPartAddedNotification), ReasoningTextDelta => "item/reasoning/textDelta" (v2::ReasoningTextDeltaNotification), + /// Notifies the user of world-writable directories on Windows, which cannot be protected by the sandbox. + WindowsWorldWritableWarning => "windows/worldWritableWarning" (v2::WindowsWorldWritableWarningNotification), + #[serde(rename = "account/login/completed")] #[ts(rename = "account/login/completed")] #[strum(serialize = "account/login/completed")] diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 67b072e669..fa1037dd14 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -934,6 +934,15 @@ pub struct McpToolCallProgressNotification { pub message: String, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct WindowsWorldWritableWarningNotification { + pub sample_paths: Vec, + pub extra_count: usize, + pub failed_scan: bool, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] diff --git a/codex-rs/app-server/Cargo.toml b/codex-rs/app-server/Cargo.toml index 96f64afdf5..84eb68c66d 100644 --- a/codex-rs/app-server/Cargo.toml +++ b/codex-rs/app-server/Cargo.toml @@ -40,6 +40,7 @@ tracing = { workspace = true, features = ["log"] } tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] } opentelemetry-appender-tracing = { workspace = true } uuid = { workspace = true, features = ["serde", "v7"] } +codex-windows-sandbox.workspace = true [dev-dependencies] app_test_support = { workspace = true } diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 3cb0d5b569..1ccc9b1293 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -111,6 +111,7 @@ use codex_core::config_loader::load_config_as_toml; use codex_core::default_client::get_codex_user_agent; use codex_core::exec::ExecParams; use codex_core::exec_env::create_env; +use codex_core::features::Feature; use codex_core::find_conversation_path_by_id_str; use codex_core::get_platform_sandbox; use codex_core::git_info::git_diff_to_remote; @@ -1249,7 +1250,17 @@ impl CodexMessageProcessor { ..Default::default() }; - let config = match derive_config_from_params(overrides, cli_overrides).await { + // Persist windows sandbox feature. + // TODO: persist default config in general. + let mut cli_overrides = cli_overrides.unwrap_or_default(); + if cfg!(target_os = "windows") && self.config.features.enabled(Feature::WindowsSandbox) { + cli_overrides.insert( + "features.enable_experimental_windows_sandbox".to_string(), + serde_json::json!(true), + ); + } + + let config = match derive_config_from_params(overrides, Some(cli_overrides)).await { Ok(config) => config, Err(err) => { let error = JSONRPCErrorError { diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index a97b037be0..55f857351a 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -6,17 +6,19 @@ use crate::outgoing_message::OutgoingMessageSender; use codex_app_server_protocol::ClientInfo; use codex_app_server_protocol::ClientRequest; use codex_app_server_protocol::InitializeResponse; - use codex_app_server_protocol::JSONRPCError; use codex_app_server_protocol::JSONRPCErrorError; use codex_app_server_protocol::JSONRPCNotification; use codex_app_server_protocol::JSONRPCRequest; use codex_app_server_protocol::JSONRPCResponse; +use codex_app_server_protocol::ServerNotification; +use codex_app_server_protocol::WindowsWorldWritableWarningNotification; use codex_core::AuthManager; use codex_core::ConversationManager; use codex_core::config::Config; use codex_core::default_client::USER_AGENT_SUFFIX; use codex_core::default_client::get_codex_user_agent; +use codex_core::features::Feature; use codex_feedback::CodexFeedback; use codex_protocol::protocol::SessionSource; use std::sync::Arc; @@ -24,6 +26,7 @@ use std::sync::Arc; pub(crate) struct MessageProcessor { outgoing: Arc, codex_message_processor: CodexMessageProcessor, + config: Arc, initialized: bool, } @@ -51,13 +54,14 @@ impl MessageProcessor { conversation_manager, outgoing.clone(), codex_linux_sandbox_exe, - config, + config.clone(), feedback, ); Self { outgoing, codex_message_processor, + config, initialized: false, } } @@ -118,6 +122,8 @@ impl MessageProcessor { self.outgoing.send_response(request_id, response).await; self.initialized = true; + self.handle_windows_world_writable_warning().await; + return; } } @@ -156,4 +162,47 @@ impl MessageProcessor { pub(crate) fn process_error(&mut self, err: JSONRPCError) { tracing::error!("<- error: {:?}", err); } + + /// On Windows, when using the experimental sandbox, we need to warn the user about world-writable directories. + async fn handle_windows_world_writable_warning(&self) { + if !cfg!(windows) { + return; + } + + if !self.config.features.enabled(Feature::WindowsSandbox) { + return; + } + + if !matches!( + self.config.sandbox_policy, + codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { .. } + | codex_protocol::protocol::SandboxPolicy::ReadOnly + ) { + return; + } + + if self + .config + .notices + .hide_world_writable_warning + .unwrap_or(false) + { + return; + } + + // This function is stubbed out to return None on non-Windows platforms + if let Some((sample_paths, extra_count, failed_scan)) = + codex_windows_sandbox::world_writable_warning_details(self.config.codex_home.as_path()) + { + self.outgoing + .send_server_notification(ServerNotification::WindowsWorldWritableWarning( + WindowsWorldWritableWarningNotification { + sample_paths, + extra_count, + failed_scan, + }, + )) + .await; + } + } }