From bcea6447ee9f0d71cac5a37b30318e3da117d15e Mon Sep 17 00:00:00 2001 From: jif Date: Thu, 6 Aug 2026 22:15:58 +0000 Subject: [PATCH] Preserve foreign cwd URIs for turn-input contributors (#37342) ## Why Converting each environment's working directory to a host-native path caused turn-input contributors to omit environments with foreign path formats. ## What changed - Expose `TurnInputEnvironment.cwd` as a `PathUri`. - Pass every turn environment to contributors without host-path conversion. ## Testing Added an integration test that submits a foreign working-directory URI and verifies the contributor receives the environment unchanged. GitOrigin-RevId: a122526be47f5331614381e9c6e4b85087a96bed --- codex-rs/Cargo.lock | 1 + codex-rs/core/src/session/turn.rs | 12 +-- .../core/tests/suite/model_visible_layout.rs | 85 +++++++++++++++++++ codex-rs/ext/extension-api/Cargo.toml | 1 + .../src/contributors/turn_input.rs | 5 +- 5 files changed, 93 insertions(+), 11 deletions(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 58b932b68e..67e9995294 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -3097,6 +3097,7 @@ dependencies = [ "codex-protocol", "codex-tools", "codex-utils-absolute-path", + "codex-utils-path-uri", "pretty_assertions", "serde_json", "tokio", diff --git a/codex-rs/core/src/session/turn.rs b/codex-rs/core/src/session/turn.rs index 107daa26e2..96ff9cf0d3 100644 --- a/codex-rs/core/src/session/turn.rs +++ b/codex-rs/core/src/session/turn.rs @@ -884,14 +884,10 @@ async fn build_extension_turn_input_items( .environments .turn_environments() .enumerate() - .filter_map(|(index, environment)| { - // TODO(anp): Migrate extension turn-input environments to PathUri so foreign cwd - // values are not omitted from extension context. - Some(TurnInputEnvironment { - environment_id: environment.environment_id.clone(), - cwd: environment.cwd().to_abs_path().ok()?.into_path_buf(), - is_primary: index == 0, - }) + .map(|(index, environment)| TurnInputEnvironment { + environment_id: environment.environment_id.clone(), + cwd: environment.cwd().clone(), + is_primary: index == 0, }) .collect::>(); diff --git a/codex-rs/core/tests/suite/model_visible_layout.rs b/codex-rs/core/tests/suite/model_visible_layout.rs index 6550e8e26b..6e64e42d28 100644 --- a/codex-rs/core/tests/suite/model_visible_layout.rs +++ b/codex-rs/core/tests/suite/model_visible_layout.rs @@ -1,13 +1,21 @@ use core_test_support::test_codex::local_selections; use std::fs; use std::sync::Arc; +use std::sync::Mutex; use anyhow::Result; use codex_config::types::Personality; use codex_core::config::Config; use codex_exec_server::LOCAL_ENVIRONMENT_ID; +use codex_extension_api::ContextualUserFragment; +use codex_extension_api::ExtensionData; +use codex_extension_api::ExtensionFuture; +use codex_extension_api::ExtensionMetrics; use codex_extension_api::ExtensionRegistry; use codex_extension_api::ExtensionRegistryBuilder; +use codex_extension_api::TurnInputContext; +use codex_extension_api::TurnInputContributor; +use codex_extension_api::TurnInputEnvironment; use codex_features::Feature; use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; @@ -35,10 +43,32 @@ use core_test_support::skip_if_no_network; use core_test_support::test_codex::test_codex; use core_test_support::test_codex::turn_permission_fields; use core_test_support::wait_for_event; +use pretty_assertions::assert_eq; use serde_json::json; const PRETURN_CONTEXT_DIFF_CWD: &str = "PRETURN_CONTEXT_DIFF_CWD"; +struct RecordingTurnInputContributor(Arc>>); + +impl TurnInputContributor for RecordingTurnInputContributor { + fn contribute<'a>( + &'a self, + input: TurnInputContext, + _extension_metrics: Option>, + _session_store: &'a ExtensionData, + _thread_store: &'a ExtensionData, + _turn_store: &'a ExtensionData, + ) -> ExtensionFuture<'a, Vec>> { + Box::pin(async move { + self.0 + .lock() + .expect("recorded environments lock") + .extend(input.environments); + Vec::new() + }) + } +} + fn skills_extensions() -> Arc> { let mut extensions = ExtensionRegistryBuilder::::new(); install(&mut extensions, |config: &Config| SkillsExtensionConfig { @@ -98,6 +128,61 @@ fn format_environment_context_subagents_snapshot(subagents: &[&str]) -> String { context_snapshot::format_response_items_snapshot(items.as_slice(), &context_snapshot_options()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn turn_input_contributors_receive_foreign_environment_cwds() -> Result<()> { + skip_if_no_network!(Ok(())); + + let server = start_mock_server().await; + let response_mock = mount_sse_once( + &server, + sse(vec![ + ev_response_created("resp-1"), + ev_assistant_message("msg-1", "done"), + ev_completed("resp-1"), + ]), + ) + .await; + let recorded_environments = Arc::new(Mutex::new(Vec::new())); + let mut extensions = ExtensionRegistryBuilder::::new(); + extensions.turn_input_contributor(Arc::new(RecordingTurnInputContributor(Arc::clone( + &recorded_environments, + )))); + let mut builder = test_codex() + .with_extensions(Arc::new(extensions.build())) + .with_config(|config| config.project_doc_max_bytes = 0); + let test = builder.build_with_auto_env(&server).await?; + let mut selection = test.executor_environment().selection().clone(); + let environment_id = selection.environment_id.clone(); + let cwd = PathUri::parse(if cfg!(windows) { + "file:///workspace" + } else { + "file:///C:/workspace" + })?; + assert!(cwd.to_abs_path().is_err()); + selection.cwd = cwd.clone(); + selection.workspace_roots = Vec::new(); + + test.submit_turn_with_environments("inspect the foreign environment", Some(vec![selection])) + .await?; + + let _request = response_mock.single_request(); + let recorded_environments = recorded_environments + .lock() + .expect("recorded environments lock") + .iter() + .map(|environment| { + ( + environment.environment_id.clone(), + environment.cwd.clone(), + environment.is_primary, + ) + }) + .collect::>(); + assert_eq!(recorded_environments, vec![(environment_id, cwd, true)]); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn model_visible_environment_context_preserves_foreign_workspace_roots() -> Result<()> { skip_if_no_network!(Ok(())); diff --git a/codex-rs/ext/extension-api/Cargo.toml b/codex-rs/ext/extension-api/Cargo.toml index 06944b471c..c6eb90ec97 100644 --- a/codex-rs/ext/extension-api/Cargo.toml +++ b/codex-rs/ext/extension-api/Cargo.toml @@ -21,6 +21,7 @@ codex-mcp = { workspace = true } codex-protocol = { workspace = true } codex-tools = { workspace = true } codex-utils-absolute-path = { workspace = true } +codex-utils-path-uri = { workspace = true } serde_json = { workspace = true } [dev-dependencies] diff --git a/codex-rs/ext/extension-api/src/contributors/turn_input.rs b/codex-rs/ext/extension-api/src/contributors/turn_input.rs index 9e4723440d..8d2411f842 100644 --- a/codex-rs/ext/extension-api/src/contributors/turn_input.rs +++ b/codex-rs/ext/extension-api/src/contributors/turn_input.rs @@ -1,6 +1,5 @@ -use std::path::PathBuf; - use codex_protocol::user_input::UserInput; +use codex_utils_path_uri::PathUri; /// Host-owned turn environment summary visible to turn-input contributors. #[derive(Debug, Clone)] @@ -8,7 +7,7 @@ pub struct TurnInputEnvironment { /// Stable host environment id used to route executor-scoped capabilities. pub environment_id: String, /// Effective working directory for this turn in the environment. - pub cwd: PathBuf, + pub cwd: PathUri, /// Whether this is the primary environment for the turn. pub is_primary: bool, }