From 1544a003db2c5c59cc8d73fc9e2ed2e8333a3e2d Mon Sep 17 00:00:00 2001 From: Adam Perry Date: Mon, 22 Jun 2026 19:17:23 +0000 Subject: [PATCH 1/7] app-server: preserve legacy cwd strings in exec events --- .../src/protocol/item_builders.rs | 27 +++++++++------- .../src/protocol/item_builders_tests.rs | 32 ++++++++++++++++++- codex-rs/core/src/tools/events.rs | 4 +-- .../remote_env_windows_test.rs | 3 +- codex-rs/core/tests/suite/unified_exec.rs | 9 ++++-- codex-rs/protocol/src/protocol.rs | 5 +-- codex-rs/rollout-trace/src/protocol_event.rs | 13 ++++---- .../rollout-trace/src/protocol_event_tests.rs | 4 +-- .../utils/path-uri/src/api_path_string.rs | 6 ++-- .../path-uri/src/api_path_string_tests.rs | 18 +++++++++++ 10 files changed, 90 insertions(+), 31 deletions(-) diff --git a/codex-rs/app-server-protocol/src/protocol/item_builders.rs b/codex-rs/app-server-protocol/src/protocol/item_builders.rs index 66019980a3c8..53c0beb9bd39 100644 --- a/codex-rs/app-server-protocol/src/protocol/item_builders.rs +++ b/codex-rs/app-server-protocol/src/protocol/item_builders.rs @@ -35,8 +35,8 @@ use codex_protocol::protocol::PatchApplyBeginEvent; use codex_protocol::protocol::PatchApplyEndEvent; use codex_shell_command::parse_command::parse_command; use codex_shell_command::parse_command::shlex_join; +use codex_utils_path_uri::LegacyAppPathString; use codex_utils_path_uri::PathConvention; -use codex_utils_path_uri::PathUri; use std::collections::HashMap; use std::path::PathBuf; use tracing::warn; @@ -90,11 +90,11 @@ pub fn build_command_execution_approval_request_item( } pub fn build_command_execution_begin_item(payload: &ExecCommandBeginEvent) -> ThreadItem { - let command_actions = command_actions_for_path_uri(&payload.parsed_cmd, &payload.cwd); + let command_actions = command_actions_for_legacy_cwd(&payload.parsed_cmd, &payload.cwd); ThreadItem::CommandExecution { id: payload.call_id.clone(), command: shlex_join(&payload.command), - cwd: payload.cwd.clone().into(), + cwd: payload.cwd.clone(), process_id: payload.process_id.clone(), source: payload.source.into(), status: CommandExecutionStatus::InProgress, @@ -112,12 +112,12 @@ pub fn build_command_execution_end_item(payload: &ExecCommandEndEvent) -> Thread Some(payload.aggregated_output.clone()) }; let duration_ms = i64::try_from(payload.duration.as_millis()).unwrap_or(i64::MAX); - let command_actions = command_actions_for_path_uri(&payload.parsed_cmd, &payload.cwd); + let command_actions = command_actions_for_legacy_cwd(&payload.parsed_cmd, &payload.cwd); ThreadItem::CommandExecution { id: payload.call_id.clone(), command: shlex_join(&payload.command), - cwd: payload.cwd.clone().into(), + cwd: payload.cwd.clone(), process_id: payload.process_id.clone(), source: payload.source.into(), status: (&payload.status).into(), @@ -128,14 +128,19 @@ pub fn build_command_execution_end_item(payload: &ExecCommandEndEvent) -> Thread } } -fn command_actions_for_path_uri(parsed_cmd: &[ParsedCommand], cwd: &PathUri) -> Vec { +fn command_actions_for_legacy_cwd( + parsed_cmd: &[ParsedCommand], + cwd: &LegacyAppPathString, +) -> Vec { // TODO(anp): Carry PathUri into CommandAction so foreign Read actions retain resolved paths. // Until then, omit those actions rather than project a foreign cwd onto the host. - let native_cwd = if cwd.infer_path_convention() == Some(PathConvention::native()) { - cwd.to_abs_path().ok() - } else { - None - }; + let native_cwd = cwd.to_inferred_path_uri().and_then(|cwd| { + if cwd.infer_path_convention() == Some(PathConvention::native()) { + cwd.to_abs_path().ok() + } else { + None + } + }); parsed_cmd .iter() diff --git a/codex-rs/app-server-protocol/src/protocol/item_builders_tests.rs b/codex-rs/app-server-protocol/src/protocol/item_builders_tests.rs index b892fcf505db..a622affc0d0f 100644 --- a/codex-rs/app-server-protocol/src/protocol/item_builders_tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/item_builders_tests.rs @@ -1,4 +1,5 @@ use super::*; +use codex_utils_path_uri::PathUri; use pretty_assertions::assert_eq; #[test] @@ -25,7 +26,7 @@ fn foreign_read_is_omitted_without_dropping_other_command_actions() { ]; assert_eq!( - command_actions_for_path_uri(&parsed_cmd, &cwd), + command_actions_for_legacy_cwd(&parsed_cmd, &cwd.into()), vec![ CommandAction::ListFiles { command: "ls".to_string(), @@ -39,3 +40,32 @@ fn foreign_read_is_omitted_without_dropping_other_command_actions() { ] ); } + +#[test] +fn raw_file_uri_cwd_is_converted_for_command_actions() { + #[cfg(windows)] + let raw_uri = "file:///C:/src"; + #[cfg(not(windows))] + let raw_uri = "file:///usr/local/src"; + let expected_path = PathUri::parse(raw_uri) + .expect("raw file URI should parse") + .to_abs_path() + .expect("raw file URI should be native") + .join("file.txt"); + let cwd = serde_json::from_value::(serde_json::json!(raw_uri)) + .expect("raw file URI should deserialize as a legacy API path"); + let parsed_cmd = vec![ParsedCommand::Read { + cmd: "cat file.txt".to_string(), + name: "file.txt".to_string(), + path: PathBuf::from("file.txt"), + }]; + + assert_eq!( + command_actions_for_legacy_cwd(&parsed_cmd, &cwd), + vec![CommandAction::Read { + command: "cat file.txt".to_string(), + name: "file.txt".to_string(), + path: expected_path, + }], + ); +} diff --git a/codex-rs/core/src/tools/events.rs b/codex-rs/core/src/tools/events.rs index 757125a03d6d..42182da3690f 100644 --- a/codex-rs/core/src/tools/events.rs +++ b/codex-rs/core/src/tools/events.rs @@ -111,7 +111,7 @@ pub(crate) async fn emit_exec_command_begin( turn_id: ctx.turn.sub_id.clone(), started_at_ms: now_unix_timestamp_ms(), command: command.to_vec(), - cwd: cwd.clone(), + cwd: cwd.clone().into(), parsed_cmd: parsed_cmd.to_vec(), source, interaction_input, @@ -551,7 +551,7 @@ async fn emit_exec_end( turn_id: ctx.turn.sub_id.clone(), completed_at_ms: now_unix_timestamp_ms(), command: exec_input.command.to_vec(), - cwd: exec_input.cwd.clone(), + cwd: exec_input.cwd.clone().into(), parsed_cmd: exec_input.parsed_cmd.to_vec(), source: exec_input.source, interaction_input: exec_input.interaction_input.map(str::to_owned), diff --git a/codex-rs/core/tests/remote_env_windows/remote_env_windows_test.rs b/codex-rs/core/tests/remote_env_windows/remote_env_windows_test.rs index 04f8914982b1..7de2e30152fc 100644 --- a/codex-rs/core/tests/remote_env_windows/remote_env_windows_test.rs +++ b/codex-rs/core/tests/remote_env_windows/remote_env_windows_test.rs @@ -194,7 +194,8 @@ async fn windows_exec_server_runs_with_native_shell_and_cwd() -> Result<()> { assert_eq!(&begin.command[1..], ["-NoProfile", "-Command", COMMAND]); let end = end.context("exec_command should emit an end event")?; - let expected_cwd = PathUri::parse("file:///C:/windows")?; + let expected_cwd: LegacyAppPathString = + PathUri::parse("file:///C:/windows")?.into(); assert_eq!((&begin.cwd, &end.cwd), (&expected_cwd, &expected_cwd)); assert_eq!((end.exit_code, end.status), (0, ExecCommandStatus::Completed)); diff --git a/codex-rs/core/tests/suite/unified_exec.rs b/codex-rs/core/tests/suite/unified_exec.rs index fe6ce7fbb7a1..196d8333ad77 100644 --- a/codex-rs/core/tests/suite/unified_exec.rs +++ b/codex-rs/core/tests/suite/unified_exec.rs @@ -437,7 +437,10 @@ async fn unified_exec_emits_exec_command_begin_event() -> Result<()> { assert_command(&begin_event.command, "-lc", "/bin/echo hello unified exec"); - assert_eq!(begin_event.cwd, PathUri::from_host_native_path(&cwd)?); + assert_eq!( + begin_event.cwd, + PathUri::from_host_native_path(&cwd)?.into() + ); wait_for_event(&test.codex, |event| { matches!(event, EventMsg::TurnComplete(_)) @@ -508,7 +511,7 @@ async fn unified_exec_resolves_relative_workdir() -> Result<()> { assert_eq!( begin_event.cwd, - PathUri::from_host_native_path(&workdir)?, + PathUri::from_host_native_path(&workdir)?.into(), "exec_command cwd should resolve relative workdir against turn cwd", ); @@ -570,7 +573,7 @@ async fn unified_exec_respects_workdir_override() -> Result<()> { assert_eq!( begin_event.cwd, - PathUri::from_host_native_path(&workdir)?, + PathUri::from_host_native_path(&workdir)?.into(), "exec_command cwd should reflect the requested workdir override" ); diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 5a73ac4c90a0..132e263ec7cd 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -54,6 +54,7 @@ use crate::request_permissions::RequestPermissionsResponse; use crate::request_user_input::RequestUserInputResponse; use crate::user_input::UserInput; use codex_utils_absolute_path::AbsolutePathBuf; +use codex_utils_path_uri::LegacyAppPathString; use codex_utils_path_uri::PathUri; use schemars::JsonSchema; use serde::Deserialize; @@ -3345,7 +3346,7 @@ pub struct ExecCommandBeginEvent { /// The command to be executed. pub command: Vec, /// The command's working directory if not the default cwd for the agent. - pub cwd: PathUri, + pub cwd: LegacyAppPathString, pub parsed_cmd: Vec, /// Where the command originated. Defaults to Agent for backward compatibility. #[serde(default)] @@ -3371,7 +3372,7 @@ pub struct ExecCommandEndEvent { /// The command that was executed. pub command: Vec, /// The command's working directory if not the default cwd for the agent. - pub cwd: PathUri, + pub cwd: LegacyAppPathString, pub parsed_cmd: Vec, /// Where the command originated. Defaults to Agent for backward compatibility. #[serde(default)] diff --git a/codex-rs/rollout-trace/src/protocol_event.rs b/codex-rs/rollout-trace/src/protocol_event.rs index 42e0db04d2bb..dac9db029df9 100644 --- a/codex-rs/rollout-trace/src/protocol_event.rs +++ b/codex-rs/rollout-trace/src/protocol_event.rs @@ -147,7 +147,7 @@ impl Serialize for ToolRuntimePayload<'_> { /// Rollout-trace representation of an exec begin event. /// /// Rollout traces share the rollout compatibility requirement that paths remain path-flavored -/// strings on disk, even though live events carry `PathUri` internally. +/// strings on disk. #[derive(Serialize)] struct ExecCommandBeginTracePayload<'a> { call_id: &'a str, @@ -156,7 +156,7 @@ struct ExecCommandBeginTracePayload<'a> { turn_id: &'a str, started_at_ms: i64, command: &'a [String], - cwd: String, + cwd: &'a str, parsed_cmd: &'a [codex_protocol::parse_command::ParsedCommand], source: ExecCommandSource, #[serde(skip_serializing_if = "Option::is_none")] @@ -182,7 +182,7 @@ impl<'a> From<&'a ExecCommandBeginEvent> for ExecCommandBeginTracePayload<'a> { turn_id, started_at_ms: *started_at_ms, command, - cwd: cwd.inferred_native_path_string(), + cwd: cwd.as_str(), parsed_cmd, source: *source, interaction_input: interaction_input.as_deref(), @@ -192,8 +192,7 @@ impl<'a> From<&'a ExecCommandBeginEvent> for ExecCommandBeginTracePayload<'a> { /// Rollout-trace representation of an exec end event. /// -/// Like [`ExecCommandBeginTracePayload`], this renders `cwd` as an inferred native path to preserve -/// the on-disk format rather than serializing the internal `PathUri`. +/// Like [`ExecCommandBeginTracePayload`], this preserves the native-path `cwd` spelling. #[derive(Serialize)] struct ExecCommandEndTracePayload<'a> { call_id: &'a str, @@ -202,7 +201,7 @@ struct ExecCommandEndTracePayload<'a> { turn_id: &'a str, completed_at_ms: i64, command: &'a [String], - cwd: String, + cwd: &'a str, parsed_cmd: &'a [codex_protocol::parse_command::ParsedCommand], source: ExecCommandSource, #[serde(skip_serializing_if = "Option::is_none")] @@ -242,7 +241,7 @@ impl<'a> From<&'a ExecCommandEndEvent> for ExecCommandEndTracePayload<'a> { turn_id, completed_at_ms: *completed_at_ms, command, - cwd: cwd.inferred_native_path_string(), + cwd: cwd.as_str(), parsed_cmd, source: *source, interaction_input: interaction_input.as_deref(), diff --git a/codex-rs/rollout-trace/src/protocol_event_tests.rs b/codex-rs/rollout-trace/src/protocol_event_tests.rs index 200c35d94d7a..3c29a5a6a57c 100644 --- a/codex-rs/rollout-trace/src/protocol_event_tests.rs +++ b/codex-rs/rollout-trace/src/protocol_event_tests.rs @@ -60,7 +60,7 @@ fn exec_command_trace_payloads_use_inferred_native_cwd() -> anyhow::Result<()> { turn_id: "turn-1".to_string(), started_at_ms: 1234, command: vec!["pwd".to_string()], - cwd: "file:///C:/windows".parse()?, + cwd: serde_json::from_value(json!(r"C:\windows"))?, parsed_cmd: Vec::new(), source: ExecCommandSource::Agent, interaction_input: None, @@ -71,7 +71,7 @@ fn exec_command_trace_payloads_use_inferred_native_cwd() -> anyhow::Result<()> { turn_id: "turn-1".to_string(), completed_at_ms: 2345, command: vec!["pwd".to_string()], - cwd: "file:///workspace/project".parse()?, + cwd: serde_json::from_value(json!("/workspace/project"))?, parsed_cmd: Vec::new(), source: ExecCommandSource::UnifiedExecInteraction, interaction_input: Some("input".to_string()), diff --git a/codex-rs/utils/path-uri/src/api_path_string.rs b/codex-rs/utils/path-uri/src/api_path_string.rs index e4fdbe7c275e..3bd9e3948a56 100644 --- a/codex-rs/utils/path-uri/src/api_path_string.rs +++ b/codex-rs/utils/path-uri/src/api_path_string.rs @@ -81,9 +81,11 @@ impl LegacyAppPathString { }) } - /// Parses this API string as an absolute path using the convention inferred from its spelling. + /// Parses this API string as a file URI or as an absolute path using its inferred convention. pub fn to_inferred_path_uri(&self) -> Option { - PathUri::try_from(self.clone()).ok() + PathUri::parse(&self.0) + .ok() + .or_else(|| PathUri::try_from(self.clone()).ok()) } /// Parses this API string as a host-native absolute path. diff --git a/codex-rs/utils/path-uri/src/api_path_string_tests.rs b/codex-rs/utils/path-uri/src/api_path_string_tests.rs index c71a65abf3df..55484f0af399 100644 --- a/codex-rs/utils/path-uri/src/api_path_string_tests.rs +++ b/codex-rs/utils/path-uri/src/api_path_string_tests.rs @@ -465,6 +465,24 @@ fn converts_absolute_api_paths_using_the_inferred_convention() { } } +#[test] +fn converts_raw_file_uris_to_inferred_path_uris() { + for raw_uri in [ + "file:///workspace/file.rs", + "file:///C:/workspace/file.rs", + "file://server/share/file.rs", + ] { + let path = serde_json::from_value::(serde_json::json!(raw_uri)) + .expect("raw file URI should deserialize as API text"); + + assert_eq!( + path.to_inferred_path_uri(), + Some(PathUri::parse(raw_uri).expect("raw file URI should parse")), + "round-tripping {raw_uri:?}", + ); + } +} + #[test] fn converts_native_api_path_to_inferred_absolute_path() { #[cfg(windows)] From f6417ec0c06c4d65c4af3152c149afbde7c3e571 Mon Sep 17 00:00:00 2001 From: Adam Perry Date: Mon, 22 Jun 2026 20:36:48 +0000 Subject: [PATCH 2/7] codex: normalize legacy URI cwd on rollout replay (#29472) --- .../app-server-protocol/src/protocol/item_builders.rs | 10 ++++++++-- .../src/protocol/item_builders_tests.rs | 8 ++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/codex-rs/app-server-protocol/src/protocol/item_builders.rs b/codex-rs/app-server-protocol/src/protocol/item_builders.rs index 53c0beb9bd39..b4dfe1909579 100644 --- a/codex-rs/app-server-protocol/src/protocol/item_builders.rs +++ b/codex-rs/app-server-protocol/src/protocol/item_builders.rs @@ -94,7 +94,7 @@ pub fn build_command_execution_begin_item(payload: &ExecCommandBeginEvent) -> Th ThreadItem::CommandExecution { id: payload.call_id.clone(), command: shlex_join(&payload.command), - cwd: payload.cwd.clone(), + cwd: normalized_app_server_cwd(&payload.cwd), process_id: payload.process_id.clone(), source: payload.source.into(), status: CommandExecutionStatus::InProgress, @@ -117,7 +117,7 @@ pub fn build_command_execution_end_item(payload: &ExecCommandEndEvent) -> Thread ThreadItem::CommandExecution { id: payload.call_id.clone(), command: shlex_join(&payload.command), - cwd: payload.cwd.clone(), + cwd: normalized_app_server_cwd(&payload.cwd), process_id: payload.process_id.clone(), source: payload.source.into(), status: (&payload.status).into(), @@ -128,6 +128,12 @@ pub fn build_command_execution_end_item(payload: &ExecCommandEndEvent) -> Thread } } +fn normalized_app_server_cwd(cwd: &LegacyAppPathString) -> LegacyAppPathString { + cwd.to_inferred_path_uri() + .map(LegacyAppPathString::from) + .unwrap_or_else(|| cwd.clone()) +} + fn command_actions_for_legacy_cwd( parsed_cmd: &[ParsedCommand], cwd: &LegacyAppPathString, diff --git a/codex-rs/app-server-protocol/src/protocol/item_builders_tests.rs b/codex-rs/app-server-protocol/src/protocol/item_builders_tests.rs index a622affc0d0f..3faaef066f44 100644 --- a/codex-rs/app-server-protocol/src/protocol/item_builders_tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/item_builders_tests.rs @@ -47,8 +47,8 @@ fn raw_file_uri_cwd_is_converted_for_command_actions() { let raw_uri = "file:///C:/src"; #[cfg(not(windows))] let raw_uri = "file:///usr/local/src"; - let expected_path = PathUri::parse(raw_uri) - .expect("raw file URI should parse") + let cwd_uri = PathUri::parse(raw_uri).expect("raw file URI should parse"); + let expected_path = cwd_uri .to_abs_path() .expect("raw file URI should be native") .join("file.txt"); @@ -60,6 +60,10 @@ fn raw_file_uri_cwd_is_converted_for_command_actions() { path: PathBuf::from("file.txt"), }]; + assert_eq!( + normalized_app_server_cwd(&cwd), + LegacyAppPathString::from(cwd_uri), + ); assert_eq!( command_actions_for_legacy_cwd(&parsed_cmd, &cwd), vec![CommandAction::Read { From 72306983f2d0a8e58aa440f6fa46384e2d503608 Mon Sep 17 00:00:00 2001 From: Adam Perry Date: Mon, 22 Jun 2026 21:28:32 +0000 Subject: [PATCH 3/7] codex: address PR review feedback (#29472) --- .../src/protocol/item_builders.rs | 2 +- codex-rs/core/src/tools/events.rs | 19 +++++++++++++++++-- codex-rs/rollout-trace/src/protocol_event.rs | 14 ++++++++++---- .../rollout-trace/src/protocol_event_tests.rs | 4 ++-- .../utils/path-uri/src/api_path_string.rs | 12 ++++++++++-- .../path-uri/src/api_path_string_tests.rs | 11 +++++++++++ 6 files changed, 51 insertions(+), 11 deletions(-) diff --git a/codex-rs/app-server-protocol/src/protocol/item_builders.rs b/codex-rs/app-server-protocol/src/protocol/item_builders.rs index b4dfe1909579..dcf18b562632 100644 --- a/codex-rs/app-server-protocol/src/protocol/item_builders.rs +++ b/codex-rs/app-server-protocol/src/protocol/item_builders.rs @@ -129,7 +129,7 @@ pub fn build_command_execution_end_item(payload: &ExecCommandEndEvent) -> Thread } fn normalized_app_server_cwd(cwd: &LegacyAppPathString) -> LegacyAppPathString { - cwd.to_inferred_path_uri() + cwd.to_file_uri() .map(LegacyAppPathString::from) .unwrap_or_else(|| cwd.clone()) } diff --git a/codex-rs/core/src/tools/events.rs b/codex-rs/core/src/tools/events.rs index 42182da3690f..5f90d846a36e 100644 --- a/codex-rs/core/src/tools/events.rs +++ b/codex-rs/core/src/tools/events.rs @@ -21,6 +21,8 @@ use codex_protocol::protocol::PatchApplyStatus; use codex_protocol::protocol::TurnDiffEvent; use codex_shell_command::parse_command::parse_command; use codex_utils_absolute_path::AbsolutePathBuf; +use codex_utils_path_uri::LegacyAppPathString; +use codex_utils_path_uri::PathConvention; use codex_utils_path_uri::PathUri; use std::collections::HashMap; use std::path::PathBuf; @@ -111,7 +113,7 @@ pub(crate) async fn emit_exec_command_begin( turn_id: ctx.turn.sub_id.clone(), started_at_ms: now_unix_timestamp_ms(), command: command.to_vec(), - cwd: cwd.clone().into(), + cwd: legacy_cwd_for_event(cwd), parsed_cmd: parsed_cmd.to_vec(), source, interaction_input, @@ -551,7 +553,7 @@ async fn emit_exec_end( turn_id: ctx.turn.sub_id.clone(), completed_at_ms: now_unix_timestamp_ms(), command: exec_input.command.to_vec(), - cwd: exec_input.cwd.clone().into(), + cwd: legacy_cwd_for_event(exec_input.cwd), parsed_cmd: exec_input.parsed_cmd.to_vec(), source: exec_input.source, interaction_input: exec_input.interaction_input.map(str::to_owned), @@ -567,6 +569,11 @@ async fn emit_exec_end( .await; } +fn legacy_cwd_for_event(cwd: &PathUri) -> LegacyAppPathString { + LegacyAppPathString::from_path_uri(cwd, PathConvention::native()) + .unwrap_or_else(|_| cwd.clone().into()) +} + async fn emit_patch_end( ctx: ToolEventCtx<'_>, changes: HashMap, @@ -816,4 +823,12 @@ mod tests { } } } + + #[cfg(unix)] + #[test] + fn event_cwd_preserves_drive_shaped_posix_path() { + let cwd = PathUri::parse("file:///C:/repo").expect("absolute POSIX path URI"); + + assert_eq!(legacy_cwd_for_event(&cwd).as_str(), "/C:/repo"); + } } diff --git a/codex-rs/rollout-trace/src/protocol_event.rs b/codex-rs/rollout-trace/src/protocol_event.rs index dac9db029df9..cd1474e788b0 100644 --- a/codex-rs/rollout-trace/src/protocol_event.rs +++ b/codex-rs/rollout-trace/src/protocol_event.rs @@ -156,7 +156,7 @@ struct ExecCommandBeginTracePayload<'a> { turn_id: &'a str, started_at_ms: i64, command: &'a [String], - cwd: &'a str, + cwd: String, parsed_cmd: &'a [codex_protocol::parse_command::ParsedCommand], source: ExecCommandSource, #[serde(skip_serializing_if = "Option::is_none")] @@ -182,7 +182,10 @@ impl<'a> From<&'a ExecCommandBeginEvent> for ExecCommandBeginTracePayload<'a> { turn_id, started_at_ms: *started_at_ms, command, - cwd: cwd.as_str(), + cwd: cwd + .to_file_uri() + .map(|cwd| cwd.inferred_native_path_string()) + .unwrap_or_else(|| cwd.as_str().to_string()), parsed_cmd, source: *source, interaction_input: interaction_input.as_deref(), @@ -201,7 +204,7 @@ struct ExecCommandEndTracePayload<'a> { turn_id: &'a str, completed_at_ms: i64, command: &'a [String], - cwd: &'a str, + cwd: String, parsed_cmd: &'a [codex_protocol::parse_command::ParsedCommand], source: ExecCommandSource, #[serde(skip_serializing_if = "Option::is_none")] @@ -241,7 +244,10 @@ impl<'a> From<&'a ExecCommandEndEvent> for ExecCommandEndTracePayload<'a> { turn_id, completed_at_ms: *completed_at_ms, command, - cwd: cwd.as_str(), + cwd: cwd + .to_file_uri() + .map(|cwd| cwd.inferred_native_path_string()) + .unwrap_or_else(|| cwd.as_str().to_string()), parsed_cmd, source: *source, interaction_input: interaction_input.as_deref(), diff --git a/codex-rs/rollout-trace/src/protocol_event_tests.rs b/codex-rs/rollout-trace/src/protocol_event_tests.rs index 3c29a5a6a57c..cfae00f00c8d 100644 --- a/codex-rs/rollout-trace/src/protocol_event_tests.rs +++ b/codex-rs/rollout-trace/src/protocol_event_tests.rs @@ -60,7 +60,7 @@ fn exec_command_trace_payloads_use_inferred_native_cwd() -> anyhow::Result<()> { turn_id: "turn-1".to_string(), started_at_ms: 1234, command: vec!["pwd".to_string()], - cwd: serde_json::from_value(json!(r"C:\windows"))?, + cwd: serde_json::from_value(json!("file:///C:/windows"))?, parsed_cmd: Vec::new(), source: ExecCommandSource::Agent, interaction_input: None, @@ -71,7 +71,7 @@ fn exec_command_trace_payloads_use_inferred_native_cwd() -> anyhow::Result<()> { turn_id: "turn-1".to_string(), completed_at_ms: 2345, command: vec!["pwd".to_string()], - cwd: serde_json::from_value(json!("/workspace/project"))?, + cwd: serde_json::from_value(json!("file:///workspace/project"))?, parsed_cmd: Vec::new(), source: ExecCommandSource::UnifiedExecInteraction, interaction_input: Some("input".to_string()), diff --git a/codex-rs/utils/path-uri/src/api_path_string.rs b/codex-rs/utils/path-uri/src/api_path_string.rs index 3bd9e3948a56..209c505a7970 100644 --- a/codex-rs/utils/path-uri/src/api_path_string.rs +++ b/codex-rs/utils/path-uri/src/api_path_string.rs @@ -81,10 +81,18 @@ impl LegacyAppPathString { }) } + /// Parses this API string as a serialized file URI. + pub fn to_file_uri(&self) -> Option { + let scheme = self.0.get(..7)?; + if !scheme.eq_ignore_ascii_case("file://") { + return None; + } + PathUri::parse(&self.0).ok() + } + /// Parses this API string as a file URI or as an absolute path using its inferred convention. pub fn to_inferred_path_uri(&self) -> Option { - PathUri::parse(&self.0) - .ok() + self.to_file_uri() .or_else(|| PathUri::try_from(self.clone()).ok()) } diff --git a/codex-rs/utils/path-uri/src/api_path_string_tests.rs b/codex-rs/utils/path-uri/src/api_path_string_tests.rs index 55484f0af399..54009e8abdaa 100644 --- a/codex-rs/utils/path-uri/src/api_path_string_tests.rs +++ b/codex-rs/utils/path-uri/src/api_path_string_tests.rs @@ -480,6 +480,17 @@ fn converts_raw_file_uris_to_inferred_path_uris() { Some(PathUri::parse(raw_uri).expect("raw file URI should parse")), "round-tripping {raw_uri:?}", ); + assert_eq!(path.to_file_uri(), path.to_inferred_path_uri()); + } +} + +#[test] +fn inferred_path_uri_rejects_relative_file_uri_aliases() { + for raw_uri in ["file:repo", "file:../repo"] { + let path = serde_json::from_value::(serde_json::json!(raw_uri)) + .expect("raw file URI should deserialize as API text"); + + assert_eq!(path.to_inferred_path_uri(), None, "parsing {raw_uri:?}"); } } From 0741417b368c46921fc241922bf6df23ac55b9dc Mon Sep 17 00:00:00 2001 From: Adam Perry Date: Mon, 22 Jun 2026 21:54:09 +0000 Subject: [PATCH 4/7] codex: fix CI failure on PR #29472 --- codex-rs/core/src/tools/events.rs | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/codex-rs/core/src/tools/events.rs b/codex-rs/core/src/tools/events.rs index 5f90d846a36e..42182da3690f 100644 --- a/codex-rs/core/src/tools/events.rs +++ b/codex-rs/core/src/tools/events.rs @@ -21,8 +21,6 @@ use codex_protocol::protocol::PatchApplyStatus; use codex_protocol::protocol::TurnDiffEvent; use codex_shell_command::parse_command::parse_command; use codex_utils_absolute_path::AbsolutePathBuf; -use codex_utils_path_uri::LegacyAppPathString; -use codex_utils_path_uri::PathConvention; use codex_utils_path_uri::PathUri; use std::collections::HashMap; use std::path::PathBuf; @@ -113,7 +111,7 @@ pub(crate) async fn emit_exec_command_begin( turn_id: ctx.turn.sub_id.clone(), started_at_ms: now_unix_timestamp_ms(), command: command.to_vec(), - cwd: legacy_cwd_for_event(cwd), + cwd: cwd.clone().into(), parsed_cmd: parsed_cmd.to_vec(), source, interaction_input, @@ -553,7 +551,7 @@ async fn emit_exec_end( turn_id: ctx.turn.sub_id.clone(), completed_at_ms: now_unix_timestamp_ms(), command: exec_input.command.to_vec(), - cwd: legacy_cwd_for_event(exec_input.cwd), + cwd: exec_input.cwd.clone().into(), parsed_cmd: exec_input.parsed_cmd.to_vec(), source: exec_input.source, interaction_input: exec_input.interaction_input.map(str::to_owned), @@ -569,11 +567,6 @@ async fn emit_exec_end( .await; } -fn legacy_cwd_for_event(cwd: &PathUri) -> LegacyAppPathString { - LegacyAppPathString::from_path_uri(cwd, PathConvention::native()) - .unwrap_or_else(|_| cwd.clone().into()) -} - async fn emit_patch_end( ctx: ToolEventCtx<'_>, changes: HashMap, @@ -823,12 +816,4 @@ mod tests { } } } - - #[cfg(unix)] - #[test] - fn event_cwd_preserves_drive_shaped_posix_path() { - let cwd = PathUri::parse("file:///C:/repo").expect("absolute POSIX path URI"); - - assert_eq!(legacy_cwd_for_event(&cwd).as_str(), "/C:/repo"); - } } From f3b157a9c8217abd04302484780ecb7d5e1a712b Mon Sep 17 00:00:00 2001 From: Adam Perry Date: Mon, 22 Jun 2026 22:26:30 +0000 Subject: [PATCH 5/7] codex: address PR review feedback (#29472) --- .../src/protocol/item_builders.rs | 10 ++-------- .../src/protocol/item_builders_tests.rs | 5 +---- codex-rs/rollout-trace/src/protocol_event.rs | 10 ++-------- codex-rs/utils/path-uri/src/api_path_string.rs | 12 ++++++++++++ codex-rs/utils/path-uri/src/api_path_string_tests.rs | 12 ++++++++---- 5 files changed, 25 insertions(+), 24 deletions(-) diff --git a/codex-rs/app-server-protocol/src/protocol/item_builders.rs b/codex-rs/app-server-protocol/src/protocol/item_builders.rs index dcf18b562632..842aefb50fd3 100644 --- a/codex-rs/app-server-protocol/src/protocol/item_builders.rs +++ b/codex-rs/app-server-protocol/src/protocol/item_builders.rs @@ -94,7 +94,7 @@ pub fn build_command_execution_begin_item(payload: &ExecCommandBeginEvent) -> Th ThreadItem::CommandExecution { id: payload.call_id.clone(), command: shlex_join(&payload.command), - cwd: normalized_app_server_cwd(&payload.cwd), + cwd: payload.cwd.normalize_file_uri(), process_id: payload.process_id.clone(), source: payload.source.into(), status: CommandExecutionStatus::InProgress, @@ -117,7 +117,7 @@ pub fn build_command_execution_end_item(payload: &ExecCommandEndEvent) -> Thread ThreadItem::CommandExecution { id: payload.call_id.clone(), command: shlex_join(&payload.command), - cwd: normalized_app_server_cwd(&payload.cwd), + cwd: payload.cwd.normalize_file_uri(), process_id: payload.process_id.clone(), source: payload.source.into(), status: (&payload.status).into(), @@ -128,12 +128,6 @@ pub fn build_command_execution_end_item(payload: &ExecCommandEndEvent) -> Thread } } -fn normalized_app_server_cwd(cwd: &LegacyAppPathString) -> LegacyAppPathString { - cwd.to_file_uri() - .map(LegacyAppPathString::from) - .unwrap_or_else(|| cwd.clone()) -} - fn command_actions_for_legacy_cwd( parsed_cmd: &[ParsedCommand], cwd: &LegacyAppPathString, diff --git a/codex-rs/app-server-protocol/src/protocol/item_builders_tests.rs b/codex-rs/app-server-protocol/src/protocol/item_builders_tests.rs index 3faaef066f44..a2556ba15ef5 100644 --- a/codex-rs/app-server-protocol/src/protocol/item_builders_tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/item_builders_tests.rs @@ -60,10 +60,7 @@ fn raw_file_uri_cwd_is_converted_for_command_actions() { path: PathBuf::from("file.txt"), }]; - assert_eq!( - normalized_app_server_cwd(&cwd), - LegacyAppPathString::from(cwd_uri), - ); + assert_eq!(cwd.normalize_file_uri(), LegacyAppPathString::from(cwd_uri)); assert_eq!( command_actions_for_legacy_cwd(&parsed_cmd, &cwd), vec![CommandAction::Read { diff --git a/codex-rs/rollout-trace/src/protocol_event.rs b/codex-rs/rollout-trace/src/protocol_event.rs index cd1474e788b0..84563cfbf7c9 100644 --- a/codex-rs/rollout-trace/src/protocol_event.rs +++ b/codex-rs/rollout-trace/src/protocol_event.rs @@ -182,10 +182,7 @@ impl<'a> From<&'a ExecCommandBeginEvent> for ExecCommandBeginTracePayload<'a> { turn_id, started_at_ms: *started_at_ms, command, - cwd: cwd - .to_file_uri() - .map(|cwd| cwd.inferred_native_path_string()) - .unwrap_or_else(|| cwd.as_str().to_string()), + cwd: cwd.normalize_file_uri().into_string(), parsed_cmd, source: *source, interaction_input: interaction_input.as_deref(), @@ -244,10 +241,7 @@ impl<'a> From<&'a ExecCommandEndEvent> for ExecCommandEndTracePayload<'a> { turn_id, completed_at_ms: *completed_at_ms, command, - cwd: cwd - .to_file_uri() - .map(|cwd| cwd.inferred_native_path_string()) - .unwrap_or_else(|| cwd.as_str().to_string()), + cwd: cwd.normalize_file_uri().into_string(), parsed_cmd, source: *source, interaction_input: interaction_input.as_deref(), diff --git a/codex-rs/utils/path-uri/src/api_path_string.rs b/codex-rs/utils/path-uri/src/api_path_string.rs index 209c505a7970..3826554c3824 100644 --- a/codex-rs/utils/path-uri/src/api_path_string.rs +++ b/codex-rs/utils/path-uri/src/api_path_string.rs @@ -83,6 +83,9 @@ impl LegacyAppPathString { /// Parses this API string as a serialized file URI. pub fn to_file_uri(&self) -> Option { + // Check the original spelling before parsing because URL parsing + // canonicalizes relative aliases such as `file:repo`, losing the + // distinction from serialized absolute file URI spellings. let scheme = self.0.get(..7)?; if !scheme.eq_ignore_ascii_case("file://") { return None; @@ -90,6 +93,15 @@ impl LegacyAppPathString { PathUri::parse(&self.0).ok() } + /// Converts a serialized file URI spelling to its inferred native path spelling. + /// + /// Non-URI path strings are returned unchanged. + pub fn normalize_file_uri(&self) -> Self { + self.to_file_uri() + .map(Self::from) + .unwrap_or_else(|| self.clone()) + } + /// Parses this API string as a file URI or as an absolute path using its inferred convention. pub fn to_inferred_path_uri(&self) -> Option { self.to_file_uri() diff --git a/codex-rs/utils/path-uri/src/api_path_string_tests.rs b/codex-rs/utils/path-uri/src/api_path_string_tests.rs index 54009e8abdaa..52766544ee08 100644 --- a/codex-rs/utils/path-uri/src/api_path_string_tests.rs +++ b/codex-rs/utils/path-uri/src/api_path_string_tests.rs @@ -467,13 +467,16 @@ fn converts_absolute_api_paths_using_the_inferred_convention() { #[test] fn converts_raw_file_uris_to_inferred_path_uris() { - for raw_uri in [ - "file:///workspace/file.rs", - "file:///C:/workspace/file.rs", - "file://server/share/file.rs", + for (raw_uri, expected_path) in [ + ("file:///workspace/file.rs", "/workspace/file.rs"), + ("file:///C:/workspace/file.rs", r"C:\workspace\file.rs"), + ("file://server/share/file.rs", r"\\server\share\file.rs"), ] { let path = serde_json::from_value::(serde_json::json!(raw_uri)) .expect("raw file URI should deserialize as API text"); + let expected_path = + serde_json::from_value::(serde_json::json!(expected_path)) + .expect("expected native path should deserialize as API text"); assert_eq!( path.to_inferred_path_uri(), @@ -481,6 +484,7 @@ fn converts_raw_file_uris_to_inferred_path_uris() { "round-tripping {raw_uri:?}", ); assert_eq!(path.to_file_uri(), path.to_inferred_path_uri()); + assert_eq!(path.normalize_file_uri(), expected_path); } } From 1b6d0bc555dd2f41861cb71133317375221b5f0c Mon Sep 17 00:00:00 2001 From: Adam Perry Date: Mon, 22 Jun 2026 23:03:47 +0000 Subject: [PATCH 6/7] codex: address PR review feedback (#29472) --- .../app-server-protocol/src/protocol/item_builders.rs | 4 ++-- .../src/protocol/item_builders_tests.rs | 5 ++++- codex-rs/rollout-trace/src/protocol_event.rs | 8 ++++++-- codex-rs/utils/path-uri/src/api_path_string.rs | 8 ++++---- codex-rs/utils/path-uri/src/api_path_string_tests.rs | 10 ++++++++-- 5 files changed, 24 insertions(+), 11 deletions(-) diff --git a/codex-rs/app-server-protocol/src/protocol/item_builders.rs b/codex-rs/app-server-protocol/src/protocol/item_builders.rs index 842aefb50fd3..0870198c7e71 100644 --- a/codex-rs/app-server-protocol/src/protocol/item_builders.rs +++ b/codex-rs/app-server-protocol/src/protocol/item_builders.rs @@ -94,7 +94,7 @@ pub fn build_command_execution_begin_item(payload: &ExecCommandBeginEvent) -> Th ThreadItem::CommandExecution { id: payload.call_id.clone(), command: shlex_join(&payload.command), - cwd: payload.cwd.normalize_file_uri(), + cwd: payload.cwd.with_serialized_file_uri_rendered_as_path(), process_id: payload.process_id.clone(), source: payload.source.into(), status: CommandExecutionStatus::InProgress, @@ -117,7 +117,7 @@ pub fn build_command_execution_end_item(payload: &ExecCommandEndEvent) -> Thread ThreadItem::CommandExecution { id: payload.call_id.clone(), command: shlex_join(&payload.command), - cwd: payload.cwd.normalize_file_uri(), + cwd: payload.cwd.with_serialized_file_uri_rendered_as_path(), process_id: payload.process_id.clone(), source: payload.source.into(), status: (&payload.status).into(), diff --git a/codex-rs/app-server-protocol/src/protocol/item_builders_tests.rs b/codex-rs/app-server-protocol/src/protocol/item_builders_tests.rs index a2556ba15ef5..0738cf3ab806 100644 --- a/codex-rs/app-server-protocol/src/protocol/item_builders_tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/item_builders_tests.rs @@ -60,7 +60,10 @@ fn raw_file_uri_cwd_is_converted_for_command_actions() { path: PathBuf::from("file.txt"), }]; - assert_eq!(cwd.normalize_file_uri(), LegacyAppPathString::from(cwd_uri)); + assert_eq!( + cwd.with_serialized_file_uri_rendered_as_path(), + LegacyAppPathString::from(cwd_uri) + ); assert_eq!( command_actions_for_legacy_cwd(&parsed_cmd, &cwd), vec![CommandAction::Read { diff --git a/codex-rs/rollout-trace/src/protocol_event.rs b/codex-rs/rollout-trace/src/protocol_event.rs index 84563cfbf7c9..3f4f37c017e4 100644 --- a/codex-rs/rollout-trace/src/protocol_event.rs +++ b/codex-rs/rollout-trace/src/protocol_event.rs @@ -182,7 +182,9 @@ impl<'a> From<&'a ExecCommandBeginEvent> for ExecCommandBeginTracePayload<'a> { turn_id, started_at_ms: *started_at_ms, command, - cwd: cwd.normalize_file_uri().into_string(), + cwd: cwd + .with_serialized_file_uri_rendered_as_path() + .into_string(), parsed_cmd, source: *source, interaction_input: interaction_input.as_deref(), @@ -241,7 +243,9 @@ impl<'a> From<&'a ExecCommandEndEvent> for ExecCommandEndTracePayload<'a> { turn_id, completed_at_ms: *completed_at_ms, command, - cwd: cwd.normalize_file_uri().into_string(), + cwd: cwd + .with_serialized_file_uri_rendered_as_path() + .into_string(), parsed_cmd, source: *source, interaction_input: interaction_input.as_deref(), diff --git a/codex-rs/utils/path-uri/src/api_path_string.rs b/codex-rs/utils/path-uri/src/api_path_string.rs index 3826554c3824..40b17cf3eb10 100644 --- a/codex-rs/utils/path-uri/src/api_path_string.rs +++ b/codex-rs/utils/path-uri/src/api_path_string.rs @@ -82,7 +82,7 @@ impl LegacyAppPathString { } /// Parses this API string as a serialized file URI. - pub fn to_file_uri(&self) -> Option { + fn parse_serialized_file_uri(&self) -> Option { // Check the original spelling before parsing because URL parsing // canonicalizes relative aliases such as `file:repo`, losing the // distinction from serialized absolute file URI spellings. @@ -96,15 +96,15 @@ impl LegacyAppPathString { /// Converts a serialized file URI spelling to its inferred native path spelling. /// /// Non-URI path strings are returned unchanged. - pub fn normalize_file_uri(&self) -> Self { - self.to_file_uri() + pub fn with_serialized_file_uri_rendered_as_path(&self) -> Self { + self.parse_serialized_file_uri() .map(Self::from) .unwrap_or_else(|| self.clone()) } /// Parses this API string as a file URI or as an absolute path using its inferred convention. pub fn to_inferred_path_uri(&self) -> Option { - self.to_file_uri() + self.parse_serialized_file_uri() .or_else(|| PathUri::try_from(self.clone()).ok()) } diff --git a/codex-rs/utils/path-uri/src/api_path_string_tests.rs b/codex-rs/utils/path-uri/src/api_path_string_tests.rs index 52766544ee08..cb6dd048bf34 100644 --- a/codex-rs/utils/path-uri/src/api_path_string_tests.rs +++ b/codex-rs/utils/path-uri/src/api_path_string_tests.rs @@ -483,8 +483,14 @@ fn converts_raw_file_uris_to_inferred_path_uris() { Some(PathUri::parse(raw_uri).expect("raw file URI should parse")), "round-tripping {raw_uri:?}", ); - assert_eq!(path.to_file_uri(), path.to_inferred_path_uri()); - assert_eq!(path.normalize_file_uri(), expected_path); + assert_eq!( + path.parse_serialized_file_uri(), + path.to_inferred_path_uri() + ); + assert_eq!( + path.with_serialized_file_uri_rendered_as_path(), + expected_path + ); } } From 22336890244be818ad69af52ddc9fa2e1c390b92 Mon Sep 17 00:00:00 2001 From: Adam Perry Date: Tue, 23 Jun 2026 00:50:14 +0000 Subject: [PATCH 7/7] codex: address PR review feedback (#29472) --- .../src/protocol/item_builders.rs | 20 +++++++++- .../src/protocol/item_builders_tests.rs | 37 +++++++++++++----- codex-rs/rollout-trace/src/protocol_event.rs | 14 ++++++- .../utils/path-uri/src/api_path_string.rs | 38 +++++++++---------- .../path-uri/src/api_path_string_tests.rs | 13 ++----- 5 files changed, 79 insertions(+), 43 deletions(-) diff --git a/codex-rs/app-server-protocol/src/protocol/item_builders.rs b/codex-rs/app-server-protocol/src/protocol/item_builders.rs index 0870198c7e71..875edffd9cdd 100644 --- a/codex-rs/app-server-protocol/src/protocol/item_builders.rs +++ b/codex-rs/app-server-protocol/src/protocol/item_builders.rs @@ -91,10 +91,18 @@ pub fn build_command_execution_approval_request_item( pub fn build_command_execution_begin_item(payload: &ExecCommandBeginEvent) -> ThreadItem { let command_actions = command_actions_for_legacy_cwd(&payload.parsed_cmd, &payload.cwd); + let cwd = payload + .cwd + .as_str() + .get(..7) + .filter(|scheme| scheme.eq_ignore_ascii_case("file://")) + .and_then(|_| payload.cwd.to_inferred_path_uri()) + .map(LegacyAppPathString::from) + .unwrap_or_else(|| payload.cwd.clone()); ThreadItem::CommandExecution { id: payload.call_id.clone(), command: shlex_join(&payload.command), - cwd: payload.cwd.with_serialized_file_uri_rendered_as_path(), + cwd, process_id: payload.process_id.clone(), source: payload.source.into(), status: CommandExecutionStatus::InProgress, @@ -113,11 +121,19 @@ pub fn build_command_execution_end_item(payload: &ExecCommandEndEvent) -> Thread }; let duration_ms = i64::try_from(payload.duration.as_millis()).unwrap_or(i64::MAX); let command_actions = command_actions_for_legacy_cwd(&payload.parsed_cmd, &payload.cwd); + let cwd = payload + .cwd + .as_str() + .get(..7) + .filter(|scheme| scheme.eq_ignore_ascii_case("file://")) + .and_then(|_| payload.cwd.to_inferred_path_uri()) + .map(LegacyAppPathString::from) + .unwrap_or_else(|| payload.cwd.clone()); ThreadItem::CommandExecution { id: payload.call_id.clone(), command: shlex_join(&payload.command), - cwd: payload.cwd.with_serialized_file_uri_rendered_as_path(), + cwd, process_id: payload.process_id.clone(), source: payload.source.into(), status: (&payload.status).into(), diff --git a/codex-rs/app-server-protocol/src/protocol/item_builders_tests.rs b/codex-rs/app-server-protocol/src/protocol/item_builders_tests.rs index 0738cf3ab806..d6f8c7f6a7ae 100644 --- a/codex-rs/app-server-protocol/src/protocol/item_builders_tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/item_builders_tests.rs @@ -1,4 +1,5 @@ use super::*; +use codex_protocol::protocol::ExecCommandSource; use codex_utils_path_uri::PathUri; use pretty_assertions::assert_eq; @@ -59,17 +60,35 @@ fn raw_file_uri_cwd_is_converted_for_command_actions() { name: "file.txt".to_string(), path: PathBuf::from("file.txt"), }]; + let payload = ExecCommandBeginEvent { + call_id: "call-1".to_string(), + process_id: None, + turn_id: "turn-1".to_string(), + started_at_ms: 0, + command: vec!["cat".to_string(), "file.txt".to_string()], + cwd, + parsed_cmd, + source: ExecCommandSource::Agent, + interaction_input: None, + }; assert_eq!( - cwd.with_serialized_file_uri_rendered_as_path(), - LegacyAppPathString::from(cwd_uri) - ); - assert_eq!( - command_actions_for_legacy_cwd(&parsed_cmd, &cwd), - vec![CommandAction::Read { + build_command_execution_begin_item(&payload), + ThreadItem::CommandExecution { + id: "call-1".to_string(), command: "cat file.txt".to_string(), - name: "file.txt".to_string(), - path: expected_path, - }], + cwd: LegacyAppPathString::from(cwd_uri), + process_id: None, + source: CommandExecutionSource::Agent, + status: CommandExecutionStatus::InProgress, + command_actions: vec![CommandAction::Read { + command: "cat file.txt".to_string(), + name: "file.txt".to_string(), + path: expected_path, + }], + aggregated_output: None, + exit_code: None, + duration_ms: None, + } ); } diff --git a/codex-rs/rollout-trace/src/protocol_event.rs b/codex-rs/rollout-trace/src/protocol_event.rs index 3f4f37c017e4..963b17c596d6 100644 --- a/codex-rs/rollout-trace/src/protocol_event.rs +++ b/codex-rs/rollout-trace/src/protocol_event.rs @@ -183,7 +183,12 @@ impl<'a> From<&'a ExecCommandBeginEvent> for ExecCommandBeginTracePayload<'a> { started_at_ms: *started_at_ms, command, cwd: cwd - .with_serialized_file_uri_rendered_as_path() + .as_str() + .get(..7) + .filter(|scheme| scheme.eq_ignore_ascii_case("file://")) + .and_then(|_| cwd.to_inferred_path_uri()) + .map(Into::into) + .unwrap_or_else(|| cwd.clone()) .into_string(), parsed_cmd, source: *source, @@ -244,7 +249,12 @@ impl<'a> From<&'a ExecCommandEndEvent> for ExecCommandEndTracePayload<'a> { completed_at_ms: *completed_at_ms, command, cwd: cwd - .with_serialized_file_uri_rendered_as_path() + .as_str() + .get(..7) + .filter(|scheme| scheme.eq_ignore_ascii_case("file://")) + .and_then(|_| cwd.to_inferred_path_uri()) + .map(Into::into) + .unwrap_or_else(|| cwd.clone()) .into_string(), parsed_cmd, source: *source, diff --git a/codex-rs/utils/path-uri/src/api_path_string.rs b/codex-rs/utils/path-uri/src/api_path_string.rs index 40b17cf3eb10..0d3ad43d7d2e 100644 --- a/codex-rs/utils/path-uri/src/api_path_string.rs +++ b/codex-rs/utils/path-uri/src/api_path_string.rs @@ -81,31 +81,15 @@ impl LegacyAppPathString { }) } - /// Parses this API string as a serialized file URI. - fn parse_serialized_file_uri(&self) -> Option { - // Check the original spelling before parsing because URL parsing - // canonicalizes relative aliases such as `file:repo`, losing the - // distinction from serialized absolute file URI spellings. - let scheme = self.0.get(..7)?; - if !scheme.eq_ignore_ascii_case("file://") { - return None; - } - PathUri::parse(&self.0).ok() - } - - /// Converts a serialized file URI spelling to its inferred native path spelling. - /// - /// Non-URI path strings are returned unchanged. - pub fn with_serialized_file_uri_rendered_as_path(&self) -> Self { - self.parse_serialized_file_uri() - .map(Self::from) - .unwrap_or_else(|| self.clone()) + fn has_serialized_file_uri_prefix(&self) -> bool { + self.0 + .get(..7) + .is_some_and(|scheme| scheme.eq_ignore_ascii_case("file://")) } /// Parses this API string as a file URI or as an absolute path using its inferred convention. pub fn to_inferred_path_uri(&self) -> Option { - self.parse_serialized_file_uri() - .or_else(|| PathUri::try_from(self.clone()).ok()) + PathUri::try_from(self.clone()).ok() } /// Parses this API string as a host-native absolute path. @@ -159,6 +143,18 @@ impl TryFrom for PathUri { type Error = LegacyAppPathStringError; fn try_from(path: LegacyAppPathString) -> Result { + // Check the original spelling before parsing because URL parsing + // canonicalizes relative aliases such as `file:repo`, losing the + // distinction from serialized absolute file URI spellings. + if path.has_serialized_file_uri_prefix() { + return PathUri::parse(path.as_str()).map_err(|_| { + LegacyAppPathStringError::InvalidNativePath { + path: path.0, + convention: None, + } + }); + } + let Some(convention) = path.infer_absolute_path_convention() else { return Err(LegacyAppPathStringError::InvalidNativePath { path: path.0, diff --git a/codex-rs/utils/path-uri/src/api_path_string_tests.rs b/codex-rs/utils/path-uri/src/api_path_string_tests.rs index cb6dd048bf34..d80a4db1fea4 100644 --- a/codex-rs/utils/path-uri/src/api_path_string_tests.rs +++ b/codex-rs/utils/path-uri/src/api_path_string_tests.rs @@ -477,20 +477,15 @@ fn converts_raw_file_uris_to_inferred_path_uris() { let expected_path = serde_json::from_value::(serde_json::json!(expected_path)) .expect("expected native path should deserialize as API text"); + let expected_uri = PathUri::parse(raw_uri).expect("raw file URI should parse"); assert_eq!( path.to_inferred_path_uri(), - Some(PathUri::parse(raw_uri).expect("raw file URI should parse")), + Some(expected_uri.clone()), "round-tripping {raw_uri:?}", ); - assert_eq!( - path.parse_serialized_file_uri(), - path.to_inferred_path_uri() - ); - assert_eq!( - path.with_serialized_file_uri_rendered_as_path(), - expected_path - ); + assert_eq!(PathUri::try_from(path), Ok(expected_uri.clone())); + assert_eq!(LegacyAppPathString::from(expected_uri), expected_path); } }