diff --git a/codex-rs/analytics/src/client.rs b/codex-rs/analytics/src/client.rs index f957a665a0..128e0bb827 100644 --- a/codex-rs/analytics/src/client.rs +++ b/codex-rs/analytics/src/client.rs @@ -512,7 +512,7 @@ impl AnalyticsEventsClient { &self, completed_at_ms: u64, request_id: RequestId, - response: RequestPermissionsResponse, + response: RequestPermissionsResponse, ) { self.record_fact(AnalyticsFact::EffectivePermissionsApprovalResponse { completed_at_ms, diff --git a/codex-rs/analytics/src/facts.rs b/codex-rs/analytics/src/facts.rs index 3346a6e19a..af535f855f 100644 --- a/codex-rs/analytics/src/facts.rs +++ b/codex-rs/analytics/src/facts.rs @@ -483,7 +483,7 @@ pub(crate) enum AnalyticsFact { EffectivePermissionsApprovalResponse { completed_at_ms: u64, request_id: RequestId, - response: Box, + response: Box>, }, ServerRequestAborted { completed_at_ms: u64, diff --git a/codex-rs/analytics/src/reducer.rs b/codex-rs/analytics/src/reducer.rs index 79649a553b..94bdfdd9ad 100644 --- a/codex-rs/analytics/src/reducer.rs +++ b/codex-rs/analytics/src/reducer.rs @@ -1118,7 +1118,7 @@ impl AnalyticsReducer { &mut self, completed_at_ms: u64, request_id: RequestId, - response: CoreRequestPermissionsResponse, + response: CoreRequestPermissionsResponse, out: &mut Vec, ) { let Some(pending_review) = self.pending_reviews.remove(&request_id) else { @@ -2229,7 +2229,7 @@ fn file_change_review_result( } fn effective_permissions_review_result( - response: &CoreRequestPermissionsResponse, + response: &CoreRequestPermissionsResponse, ) -> (ReviewStatus, ReviewResolution) { if response.permissions.is_empty() { return (ReviewStatus::Denied, ReviewResolution::None); diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index a1d3b48862..26ae66fdec 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -540,10 +540,7 @@ pub(crate) async fn apply_bespoke_event_handling( item_id: item_id.clone(), started_at_ms: event.started_at_ms, reason: event.reason.clone(), - grant_root: event - .grant_root - .as_deref() - .map(LegacyAppPathString::from_path), + grant_root: event.grant_root.clone(), }; let (pending_request_id, rx) = outgoing .send_request(ServerRequestPayload::FileChangeRequestApproval(params)) diff --git a/codex-rs/app-server/src/outgoing_message.rs b/codex-rs/app-server/src/outgoing_message.rs index 16d5c2ff9d..cf2f2a0851 100644 --- a/codex-rs/app-server/src/outgoing_message.rs +++ b/codex-rs/app-server/src/outgoing_message.rs @@ -18,6 +18,7 @@ use codex_otel::span_w3c_trace_context; use codex_protocol::ThreadId; use codex_protocol::protocol::W3cTraceContext; use codex_protocol::request_permissions::RequestPermissionsResponse; +use codex_utils_path_uri::PathUri; use tokio::sync::Mutex; use tokio::sync::mpsc; use tokio::sync::oneshot; @@ -146,14 +147,14 @@ impl ThreadScopedOutgoingMessageSender { pub(crate) fn track_effective_permissions_approval_response( &self, request_id: RequestId, - response: RequestPermissionsResponse, + response: RequestPermissionsResponse, ) { self.outgoing .analytics_events_client .track_effective_permissions_approval_response( now_unix_timestamp_ms(), request_id, - response, + response.map_paths(|path| path.inferred_native_path_string()), ); } diff --git a/codex-rs/app-server/src/request_processors/catalog_processor.rs b/codex-rs/app-server/src/request_processors/catalog_processor.rs index 9542340200..0b5435e354 100644 --- a/codex-rs/app-server/src/request_processors/catalog_processor.rs +++ b/codex-rs/app-server/src/request_processors/catalog_processor.rs @@ -1,5 +1,7 @@ use super::*; use codex_core::config::permission_profile_catalog; +use codex_utils_path_uri::LegacyAppPathString; +use codex_utils_path_uri::PathConvention; use futures::StreamExt; #[derive(Clone)] @@ -15,6 +17,21 @@ pub(crate) struct CatalogRequestProcessor { const SKILLS_LIST_CWD_CONCURRENCY: usize = 5; +fn localize_catalog_cwd(cwd: &LegacyAppPathString) -> Result { + match cwd.to_host_abs_path() { + Ok(cwd) => return Ok(cwd), + Err(host_err) if cwd.infer_absolute_path_convention() == Some(PathConvention::native()) => { + return Err(host_err.to_string()); + } + Err(_) => {} + } + if let Some(convention) = cwd.infer_absolute_path_convention() { + let path_uri = cwd.to_path_uri(convention).map_err(|err| err.to_string())?; + return Err(format!("invalid cwd: foreign path URI {path_uri}")); + } + AbsolutePathBuf::relative_to_current_dir(cwd.as_str()).map_err(|err| err.to_string()) +} + fn skills_to_info( skills: &[codex_core::skills::SkillMetadata], disabled_paths: &HashSet, @@ -421,9 +438,10 @@ impl CatalogRequestProcessor { let PermissionProfileListParams { cursor, limit, cwd } = params; let config_layer_stack = match cwd { Some(cwd) => { - let cwd = PathBuf::from(cwd); + let cwd = localize_catalog_cwd(&cwd) + .map_err(|err| invalid_request(format!("invalid cwd: {err}")))?; let (_, config_layer_stack) = self - .resolve_cwd_config(&cwd) + .resolve_cwd_config(cwd.as_path()) .await .map_err(|err| internal_error(format!("failed to reload config: {err}")))?; config_layer_stack diff --git a/codex-rs/app-server/src/request_processors/command_exec_processor.rs b/codex-rs/app-server/src/request_processors/command_exec_processor.rs index dfe7555d58..1510ec1eba 100644 --- a/codex-rs/app-server/src/request_processors/command_exec_processor.rs +++ b/codex-rs/app-server/src/request_processors/command_exec_processor.rs @@ -233,7 +233,11 @@ impl CommandExecRequestProcessor { config.managed_network_requirements_enabled(), config.effective_workspace_roots(), ) - } else if let Some(policy) = sandbox_policy.map(|policy| policy.to_core()) { + } else if let Some(policy) = sandbox_policy + .map(|policy| policy.try_to_core()) + .transpose() + .map_err(|err| invalid_request(format!("invalid sandbox policy: {err}")))? + { self.config .permissions .can_set_legacy_sandbox_policy(&policy, &sandbox_cwd) diff --git a/codex-rs/app-server/src/request_processors/turn_processor.rs b/codex-rs/app-server/src/request_processors/turn_processor.rs index 3ff64cd34e..0bc363f692 100644 --- a/codex-rs/app-server/src/request_processors/turn_processor.rs +++ b/codex-rs/app-server/src/request_processors/turn_processor.rs @@ -660,7 +660,10 @@ impl TurnRequestProcessor { approval_policy.map(codex_app_server_protocol::AskForApproval::to_core); let approvals_reviewer = approvals_reviewer.map(codex_app_server_protocol::ApprovalsReviewer::to_core); - let sandbox_policy = sandbox_policy.map(|policy| policy.to_core()); + let sandbox_policy = sandbox_policy + .map(|policy| policy.try_to_core()) + .transpose() + .map_err(|err| invalid_request(format!("invalid sandbox policy: {err}")))?; let (permission_profile, active_permission_profile, profile_workspace_roots) = if let Some(permissions) = permissions { let Some(snapshot) = snapshot.as_ref() else { diff --git a/codex-rs/app-server/tests/suite/v2/permission_profile_list.rs b/codex-rs/app-server/tests/suite/v2/permission_profile_list.rs index d291dda172..50eaf3ea2b 100644 --- a/codex-rs/app-server/tests/suite/v2/permission_profile_list.rs +++ b/codex-rs/app-server/tests/suite/v2/permission_profile_list.rs @@ -13,6 +13,7 @@ use codex_protocol::config_types::TrustLevel; use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS; use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_READ_ONLY; use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE; +use codex_utils_path_uri::LegacyAppPathString; use pretty_assertions::assert_eq; use tempfile::TempDir; use tokio::time::timeout; @@ -128,7 +129,7 @@ description = "Project-scoped profile." .send_permission_profile_list_request(PermissionProfileListParams { cursor: None, limit: Some(3), - cwd: Some(workspace.path().to_string_lossy().into_owned()), + cwd: Some(LegacyAppPathString::from_path(workspace.path())), }) .await?; let first = read_response::(&mut mcp, first_request_id).await?; @@ -160,7 +161,7 @@ description = "Project-scoped profile." .send_permission_profile_list_request(PermissionProfileListParams { cursor: first.next_cursor, limit: Some(3), - cwd: Some(workspace.path().to_string_lossy().into_owned()), + cwd: Some(LegacyAppPathString::from_path(workspace.path())), }) .await?; let second = @@ -209,7 +210,7 @@ description = "Project-scoped profile." .send_permission_profile_list_request(PermissionProfileListParams { cursor: None, limit: None, - cwd: Some(workspace.path().to_string_lossy().into_owned()), + cwd: Some(LegacyAppPathString::from_path(workspace.path())), }) .await?; let actual = read_response::(&mut mcp, request_id).await?; diff --git a/codex-rs/app-server/tests/suite/v2/request_permissions.rs b/codex-rs/app-server/tests/suite/v2/request_permissions.rs index b30e63edb4..63984477f8 100644 --- a/codex-rs/app-server/tests/suite/v2/request_permissions.rs +++ b/codex-rs/app-server/tests/suite/v2/request_permissions.rs @@ -87,7 +87,7 @@ async fn request_permissions_round_trip() -> Result<()> { assert_eq!(params.thread_id, thread.id); assert_eq!(params.turn_id, turn.id); assert_eq!(params.item_id, "call1"); - assert!(params.cwd.as_path().is_absolute()); + assert!(params.cwd.to_host_abs_path()?.is_absolute()); assert_eq!(params.reason, Some("Select a workspace root".to_string())); let requested_file_system = params .permissions diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs index 62b13a1e52..e9f3650595 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -78,6 +78,7 @@ use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::protocol::MULTI_AGENT_MODE_OPEN_TAG; use codex_protocol::user_input::MAX_USER_INPUT_TEXT_CHARS; use codex_utils_absolute_path::test_support::PathExt; +use codex_utils_path_uri::LegacyAppPathString; use core_test_support::responses; use core_test_support::skip_if_no_network; use pretty_assertions::assert_eq; @@ -2599,7 +2600,7 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> { approval_policy: Some(codex_app_server_protocol::AskForApproval::Never), approvals_reviewer: None, sandbox_policy: Some(codex_app_server_protocol::SandboxPolicy::WorkspaceWrite { - writable_roots: vec![first_cwd.try_into()?], + writable_roots: vec![LegacyAppPathString::from_path(&first_cwd)], network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true,