diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 0f41427301..9d5fc872f7 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -302,6 +302,7 @@ Example with notification opt-out: - `mcpServer/event/stream/stop` (experimental) — stop the caller's event subscription by `subscriptionId`. - `mcpServer/tool/call` — call a tool on a thread's configured MCP server by `threadId`, `server`, `tool`, optional `arguments`, and optional `_meta`, returning the MCP tool result. Parent-owned Multi-Agent V2 subagents reject direct tool calls. - `windowsSandbox/setupStart` — start Windows sandbox setup for the selected mode (`elevated` or `unelevated`); accepts an optional absolute `cwd` to target setup for a specific workspace, returns `{ started: true }` immediately, and later emits `windowsSandbox/setupCompleted`. + The default-off `windows_sandbox_service` feature enables attempting service provisioning for elevated setup. Clients can set it through `experimentalFeature/enablement/set` before starting setup; when disabled, setup uses the existing elevated helper directly. - `feedback/upload` — submit a feedback report (classification + optional reason/logs, conversation_id, and optional `extraLogFiles` attachments array); returns the tracking thread id. With logs enabled, includes bounded recent failed Guardian review actions, decisions, and reviewer history from the reported thread and its descendants, linked to the reviewed turn and target item where available. Rollout selection preserves the reported thread and prioritizes children with retained failed reviews before newer children, including each selected thread's available Guardian trunk rollout. `feedback-thread-index.json` lists selected filenames and bounded omission details; it describes selection, not successful delivery. Failed-review captures are process-local, so missing evidence does not establish that no denial occurred. - `config/read` — fetch the runtime-effective config after resolving config layering and managed requirements, including opaque `desktop` values stored in `config.toml`. When configured, the `packagedDefaults` layer has the lowest precedence. - `externalAgentConfig/detect` — detect migratable external-agent artifacts with `includeHome`, optional `cwds`, and an optional `migrationSource` selector. Omitted, `null`, or unrecognized migration-source values retain the default behavior. The deprecated optional `source` field remains accepted for compatibility but does not select the migration source. Each detected item includes `cwd` (`null` for home), and multi-item migrations may additionally include structured `details` with plugin ids, skill names, memory, session metadata, or other artifact names. The response also includes connector candidates inferred from detected source sessions, with a normalized display `name`, the number of detected sessions that used the connector, and the source metadata field used for detection. diff --git a/codex-rs/app-server/src/request_processors/config_processor.rs b/codex-rs/app-server/src/request_processors/config_processor.rs index 278fc8e050..61eb09492a 100644 --- a/codex-rs/app-server/src/request_processors/config_processor.rs +++ b/codex-rs/app-server/src/request_processors/config_processor.rs @@ -71,6 +71,7 @@ const SUPPORTED_EXPERIMENTAL_FEATURE_ENABLEMENT: &[&str] = &[ "remote_control", "remote_plugin", "tool_suggest", + "windows_sandbox_service", ]; #[derive(Clone)] diff --git a/codex-rs/app-server/src/request_processors/windows_sandbox_processor.rs b/codex-rs/app-server/src/request_processors/windows_sandbox_processor.rs index c0917d933b..6e454e13f7 100644 --- a/codex-rs/app-server/src/request_processors/windows_sandbox_processor.rs +++ b/codex-rs/app-server/src/request_processors/windows_sandbox_processor.rs @@ -83,8 +83,82 @@ impl WindowsSandboxRequestProcessor { env_map: std::env::vars().collect(), codex_home: config.codex_home.to_path_buf(), }; - let setup_result = - codex_core::windows_sandbox::run_windows_sandbox_setup(setup_request).await; + let setup_result = async { + // Workload identity is process-local, so use the existing helper path with + // the caller's resolved configuration instead of loading auth in the service. + #[cfg(target_os = "windows")] + if setup_mode == CoreWindowsSandboxSetupMode::Elevated + && config.features.enabled(Feature::WindowsSandboxService) + && !codex_login::is_workload_identity_selected() + { + let provisioning = match config + .permissions + .network + .as_ref() + .map_or_else( + || { + Ok(( + codex_windows_sandbox::WindowsSandboxProvisioningSettings::from_environment( + &setup_request.permission_profile, + &setup_request.env_map, + ), + codex_windows_sandbox::WindowsSandboxProxyListeners::from_environment( + &setup_request.permission_profile, + &setup_request.env_map, + ), + )) + }, + codex_core::config::NetworkProxySpec::windows_sandbox_proxy_listeners, + ) + { + Ok(provisioning) => Some(provisioning), + Err(error) => { + warn!( + "Windows sandbox service does not support the configured proxy listeners; falling back to elevated setup: {error}" + ); + None + } + }; + if let Some((settings, listeners)) = provisioning { + let service_setup_request = setup_request.clone(); + let service_setup_start = Instant::now(); + tokio::task::spawn_blocking(move || -> anyhow::Result<()> { + if codex_windows_sandbox::ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots( + &service_setup_request.permission_profile, + &service_setup_request.workspace_roots, + ).is_err() { + // The existing setup path can still succeed for completed + // provisioning without resolving the current profile. + return Ok(()); + } + // The shared setup path below handles helper fallback and + // refreshes workspace ACLs after provisioning. + codex_windows_sandbox::provision_windows_sandbox_via_service( + &service_setup_request.codex_home, + settings, + listeners, + )?; + Ok(()) + }) + .await + .map_err(|error| { + anyhow::anyhow!( + "Windows sandbox service provisioning task failed: {error}" + ) + }) + .and_then(std::convert::identity) + .inspect_err(|error| { + codex_core::windows_sandbox::emit_windows_sandbox_setup_failure_metrics( + setup_mode, + service_setup_start.elapsed(), + error, + ); + })?; + } + } + codex_core::windows_sandbox::run_windows_sandbox_setup(setup_request).await + } + .await; let notification = WindowsSandboxSetupCompletedNotification { mode: match setup_mode { CoreWindowsSandboxSetupMode::Elevated => WindowsSandboxSetupMode::Elevated, diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 2b04ea4b81..3f95d69f84 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -1041,6 +1041,9 @@ "web_search_request": { "type": "boolean" }, + "windows_sandbox_service": { + "type": "boolean" + }, "workspace_dependencies": { "type": "boolean" }, @@ -6353,6 +6356,9 @@ "web_search_request": { "type": "boolean" }, + "windows_sandbox_service": { + "type": "boolean" + }, "workspace_dependencies": { "type": "boolean" }, diff --git a/codex-rs/core/src/config/network_proxy_spec.rs b/codex-rs/core/src/config/network_proxy_spec.rs index d7921ac05f..c2235d6448 100644 --- a/codex-rs/core/src/config/network_proxy_spec.rs +++ b/codex-rs/core/src/config/network_proxy_spec.rs @@ -98,6 +98,46 @@ impl NetworkProxySpec { self.config.allow_local_binding } + /// Returns the firewall settings and listener identities used by the Windows provisioning service. + #[cfg(target_os = "windows")] + pub fn windows_sandbox_proxy_listeners( + &self, + ) -> std::io::Result<( + codex_windows_sandbox::WindowsSandboxProvisioningSettings, + codex_windows_sandbox::WindowsSandboxProxyListeners, + )> { + if !self.config.enabled { + return Ok(( + codex_windows_sandbox::WindowsSandboxProvisioningSettings::default(), + codex_windows_sandbox::WindowsSandboxProxyListeners::default(), + )); + } + + let proxy_ports = self.configured_proxy_ports()?; + let http_port = self + .proxy_host_and_port() + .rsplit_once(':') + .and_then(|(_, port)| port.parse::().ok()) + .ok_or_else(|| std::io::Error::other("invalid HTTP proxy listener port"))?; + let socks_port = self.config.enable_socks5.then(|| { + proxy_ports + .iter() + .copied() + .find(|port| *port != http_port) + .unwrap_or(http_port) + }); + Ok(( + codex_windows_sandbox::WindowsSandboxProvisioningSettings { + proxy_ports, + allow_local_binding: self.config.allow_local_binding, + }, + codex_windows_sandbox::WindowsSandboxProxyListeners { + http_ports: vec![http_port], + socks_ports: socks_port.into_iter().collect(), + }, + )) + } + pub fn from_config_and_constraints( config: NetworkProxyConfig, requirements: Option, diff --git a/codex-rs/core/src/config/network_proxy_spec_tests.rs b/codex-rs/core/src/config/network_proxy_spec_tests.rs index d069a1f2e4..4afa5cc3cf 100644 --- a/codex-rs/core/src/config/network_proxy_spec_tests.rs +++ b/codex-rs/core/src/config/network_proxy_spec_tests.rs @@ -44,6 +44,38 @@ fn build_state_with_audit_metadata_threads_metadata_to_state() { assert_eq!(state.audit_metadata(), &metadata); } +#[cfg(target_os = "windows")] +#[test] +fn windows_sandbox_proxy_listeners_preserve_effective_protocol_roles() { + let spec = NetworkProxySpec::from_config_and_constraints( + NetworkProxyConfig { + enabled: true, + proxy_url: "http://127.0.0.1:48081".to_string(), + socks_url: "socks5h://127.0.0.1:3128".to_string(), + allow_local_binding: true, + ..NetworkProxyConfig::default() + }, + /*requirements*/ None, + &PermissionProfile::workspace_write(), + ) + .expect("effective network configuration should be valid"); + + assert_eq!( + spec.windows_sandbox_proxy_listeners() + .expect("effective proxy listeners should resolve"), + ( + codex_windows_sandbox::WindowsSandboxProvisioningSettings { + proxy_ports: vec![3128, 48081], + allow_local_binding: true, + }, + codex_windows_sandbox::WindowsSandboxProxyListeners { + http_ports: vec![48081], + socks_ports: vec![3128], + }, + ) + ); +} + #[test] fn environment_policy_replaces_soft_controller_allowlist_and_preserves_denials() { let requirements = NetworkConstraints { diff --git a/codex-rs/core/src/windows_sandbox.rs b/codex-rs/core/src/windows_sandbox.rs index 73056ff1e8..72576c2916 100644 --- a/codex-rs/core/src/windows_sandbox.rs +++ b/codex-rs/core/src/windows_sandbox.rs @@ -268,12 +268,7 @@ pub async fn run_windows_sandbox_setup(request: WindowsSandboxSetupRequest) -> a Ok(()) } Err(err) => { - emit_windows_sandbox_setup_failure_metrics( - mode, - originator_tag.as_str(), - start.elapsed(), - &err, - ); + emit_windows_sandbox_setup_failure_metrics(mode, start.elapsed(), &err); Err(err) } } @@ -351,15 +346,17 @@ fn emit_windows_sandbox_setup_success_metrics( ); } -fn emit_windows_sandbox_setup_failure_metrics( +/// Records setup failures, including service attempts that fail before the helper path. +pub fn emit_windows_sandbox_setup_failure_metrics( mode: WindowsSandboxSetupMode, - originator_tag: &str, duration: std::time::Duration, _err: &anyhow::Error, ) { let Some(metrics) = codex_otel::global() else { return; }; + let originator_tag = sanitize_metric_tag_value(originator().value.as_str()); + let originator_tag = originator_tag.as_str(); let mode_tag = windows_sandbox_setup_mode_tag(mode); let _ = metrics.record_duration( "codex.windows_sandbox.setup_duration_ms", diff --git a/codex-rs/features/src/lib.rs b/codex-rs/features/src/lib.rs index ec463fefc4..1cf7523478 100644 --- a/codex-rs/features/src/lib.rs +++ b/codex-rs/features/src/lib.rs @@ -364,6 +364,8 @@ pub enum Feature { WindowsSandbox, /// Use the elevated Windows sandbox pipeline (setup + runner). WindowsSandboxElevated, + /// Attempt elevated Windows sandbox provisioning through the installed service. + WindowsSandboxService, /// Legacy remote models flag kept for backward compatibility. RemoteModels, /// Removed legacy git commit attribution guidance flag. @@ -1171,6 +1173,12 @@ pub const FEATURES: &[FeatureSpec] = &[ stage: Stage::Removed, default_enabled: false, }, + FeatureSpec { + id: Feature::WindowsSandboxService, + key: "windows_sandbox_service", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, FeatureSpec { id: Feature::RemoteModels, key: "remote_models", diff --git a/codex-rs/windows-sandbox-service/src/ipc.rs b/codex-rs/windows-sandbox-service/src/ipc.rs index ebc639628a..101628b3ff 100644 --- a/codex-rs/windows-sandbox-service/src/ipc.rs +++ b/codex-rs/windows-sandbox-service/src/ipc.rs @@ -1,5 +1,6 @@ //! Authenticated local IPC for the Windows sandbox provisioning service. -//! Configuration parse failures defer provisioning to the client's elevated helper. +//! Configuration parse failures and unsupported home drives defer provisioning to +//! the client's elevated helper. //! Shutdown wakeups are retried until the listener connects or stops. mod authentication; @@ -125,6 +126,9 @@ pub(crate) fn run(shutdown: Arc, on_ready: impl FnOnce() -> Result<( let result = handle_request(pipe.0, &authorized_process, &sandbox_sid, &shutdown); let response = match result { Ok(response) => response, + Err(error) if error.is::() => { + SandboxProvisioningResponse::Unavailable + } Err(error) => { eprintln!("sandbox provisioning request failed: {error}"); let mut message = String::new(); diff --git a/codex-rs/windows-sandbox-service/src/ipc/home.rs b/codex-rs/windows-sandbox-service/src/ipc/home.rs index e8a0c3f295..aacd1c882b 100644 --- a/codex-rs/windows-sandbox-service/src/ipc/home.rs +++ b/codex-rs/windows-sandbox-service/src/ipc/home.rs @@ -21,6 +21,18 @@ use windows_sys::Win32::Storage::FileSystem as filesystem; const DRIVE_FIXED: u32 = 3; +/// The service does not support this drive; the interactive helper may still work. +#[derive(Debug)] +pub(super) struct UnsupportedHomeDrive; + +impl std::fmt::Display for UnsupportedHomeDrive { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("Codex home must be located on a fixed local drive") + } +} + +impl std::error::Error for UnsupportedHomeDrive {} + pub(super) struct OwnedHandle(pub(super) HANDLE); impl Drop for OwnedHandle { @@ -41,7 +53,7 @@ pub(super) fn prepare_codex_home(requested: &Path) -> Result<(PathBuf, Vec Result<(PathBuf, Vec