protocol: retain executor cwd in command events

This commit is contained in:
Adam Perry
2026-06-13 06:26:43 +00:00
parent 385e9e546a
commit 7d8a9ef36b
13 changed files with 275 additions and 85 deletions
@@ -23,6 +23,7 @@ use crate::protocol::v2::PatchApplyStatus;
use crate::protocol::v2::PatchChangeKind;
use crate::protocol::v2::ThreadItem;
use codex_protocol::ThreadId;
use codex_protocol::parse_command::ParsedCommand;
use codex_protocol::protocol::ApplyPatchApprovalRequestEvent;
use codex_protocol::protocol::ExecApprovalRequestEvent;
use codex_protocol::protocol::ExecCommandBeginEvent;
@@ -42,8 +43,37 @@ use std::collections::HashMap;
use std::path::PathBuf;
pub fn native_command_cwd(cwd: &AbsolutePathBuf) -> ApiPathString {
ApiPathString::from_path_uri(&PathUri::from_abs_path(cwd), PathConvention::native())
.unwrap_or_else(|_| ApiPathString::new(cwd.to_string_lossy().into_owned()))
native_command_cwd_from_uri(&PathUri::from_abs_path(cwd), PathConvention::native())
}
fn native_command_cwd_from_uri(cwd: &PathUri, path_convention: PathConvention) -> ApiPathString {
ApiPathString::from_path_uri(cwd, path_convention)
.unwrap_or_else(|_| ApiPathString::new(cwd.to_string()))
}
fn command_actions_for_event(
parsed_cmd: &[ParsedCommand],
cwd: &PathUri,
path_convention: PathConvention,
) -> Vec<CommandAction> {
let host_cwd = (path_convention == PathConvention::native())
.then(|| cwd.to_abs_path().ok())
.flatten();
parsed_cmd
.iter()
.cloned()
.map(|parsed| match host_cwd.as_ref() {
Some(cwd) => CommandAction::from_core_with_cwd(parsed, cwd),
None => CommandAction::Unknown {
command: match parsed {
ParsedCommand::Read { cmd, .. }
| ParsedCommand::ListFiles { cmd, .. }
| ParsedCommand::Search { cmd, .. }
| ParsedCommand::Unknown { cmd } => cmd,
},
},
})
.collect()
}
pub fn build_file_change_approval_request_item(
@@ -98,16 +128,15 @@ pub fn build_command_execution_begin_item(payload: &ExecCommandBeginEvent) -> Th
ThreadItem::CommandExecution {
id: payload.call_id.clone(),
command: shlex_join(&payload.command),
cwd: native_command_cwd(&payload.cwd),
cwd: native_command_cwd_from_uri(&payload.cwd, payload.path_convention),
process_id: payload.process_id.clone(),
source: payload.source.into(),
status: CommandExecutionStatus::InProgress,
command_actions: payload
.parsed_cmd
.iter()
.cloned()
.map(|parsed| CommandAction::from_core_with_cwd(parsed, &payload.cwd))
.collect(),
command_actions: command_actions_for_event(
&payload.parsed_cmd,
&payload.cwd,
payload.path_convention,
),
aggregated_output: None,
exit_code: None,
duration_ms: None,
@@ -125,16 +154,15 @@ pub fn build_command_execution_end_item(payload: &ExecCommandEndEvent) -> Thread
ThreadItem::CommandExecution {
id: payload.call_id.clone(),
command: shlex_join(&payload.command),
cwd: native_command_cwd(&payload.cwd),
cwd: native_command_cwd_from_uri(&payload.cwd, payload.path_convention),
process_id: payload.process_id.clone(),
source: payload.source.into(),
status: (&payload.status).into(),
command_actions: payload
.parsed_cmd
.iter()
.cloned()
.map(|parsed| CommandAction::from_core_with_cwd(parsed, &payload.cwd))
.collect(),
command_actions: command_actions_for_event(
&payload.parsed_cmd,
&payload.cwd,
payload.path_convention,
),
aggregated_output,
exit_code: Some(payload.exit_code),
duration_ms: Some(duration_ms),
@@ -324,3 +352,7 @@ fn format_file_change_diff(change: &FileChange) -> String {
}
}
}
#[cfg(test)]
#[path = "item_builders_tests.rs"]
mod tests;
@@ -0,0 +1,97 @@
use super::*;
use codex_protocol::parse_command::ParsedCommand;
use codex_protocol::protocol::ExecCommandBeginEvent;
use codex_protocol::protocol::ExecCommandSource;
use pretty_assertions::assert_eq;
use std::path::PathBuf;
fn begin_event(
cwd: PathUri,
path_convention: PathConvention,
parsed_cmd: Vec<ParsedCommand>,
) -> ExecCommandBeginEvent {
ExecCommandBeginEvent {
call_id: "exec-1".to_string(),
process_id: None,
turn_id: "turn-1".to_string(),
started_at_ms: 0,
command: vec!["cat".to_string(), "notes.txt".to_string()],
cwd,
path_convention,
parsed_cmd,
source: ExecCommandSource::Agent,
interaction_input: None,
}
}
#[test]
fn windows_command_event_renders_windows_native_cwd() {
let event = begin_event(
PathUri::parse("file:///C:/Research/space%20%23%25").expect("Windows cwd URI"),
PathConvention::Windows,
vec![ParsedCommand::Unknown {
cmd: "cat notes.txt".to_string(),
}],
);
assert_eq!(
build_command_execution_begin_item(&event),
ThreadItem::CommandExecution {
id: "exec-1".to_string(),
command: "cat notes.txt".to_string(),
cwd: ApiPathString::new(r"C:\Research\space #%"),
process_id: None,
source: CommandExecutionSource::Agent,
status: CommandExecutionStatus::InProgress,
command_actions: vec![CommandAction::Unknown {
command: "cat notes.txt".to_string(),
}],
aggregated_output: None,
exit_code: None,
duration_ms: None,
}
);
}
#[test]
fn foreign_command_event_does_not_project_read_path_onto_host() {
let (cwd, path_convention, native_cwd) = match PathConvention::native() {
PathConvention::Posix => (
PathUri::parse("file:///C:/workspace").expect("Windows cwd URI"),
PathConvention::Windows,
ApiPathString::new(r"C:\workspace"),
),
PathConvention::Windows => (
PathUri::parse("file:///workspace").expect("POSIX cwd URI"),
PathConvention::Posix,
ApiPathString::new("/workspace"),
),
};
let event = begin_event(
cwd,
path_convention,
vec![ParsedCommand::Read {
cmd: "cat notes.txt".to_string(),
name: "notes.txt".to_string(),
path: PathBuf::from("notes.txt"),
}],
);
assert_eq!(
build_command_execution_begin_item(&event),
ThreadItem::CommandExecution {
id: "exec-1".to_string(),
command: "cat notes.txt".to_string(),
cwd: native_cwd,
process_id: None,
source: CommandExecutionSource::Agent,
status: CommandExecutionStatus::InProgress,
command_actions: vec![CommandAction::Unknown {
command: "cat notes.txt".to_string(),
}],
aggregated_output: None,
exit_code: None,
duration_ms: None,
}
);
}
@@ -1266,6 +1266,8 @@ mod tests {
use codex_protocol::protocol::WebSearchEndEvent;
use codex_utils_absolute_path::test_support::PathBufExt;
use codex_utils_absolute_path::test_support::test_path_buf;
use codex_utils_path_uri::PathConvention;
use codex_utils_path_uri::PathUri;
use pretty_assertions::assert_eq;
use std::path::PathBuf;
use std::time::Duration;
@@ -2013,7 +2015,8 @@ mod tests {
turn_id: "turn-1".into(),
completed_at_ms: 0,
command: vec!["echo".into(), "hello world".into()],
cwd: test_path_buf("/tmp").abs(),
cwd: PathUri::from_abs_path(&test_path_buf("/tmp").abs()),
path_convention: PathConvention::native(),
parsed_cmd: vec![ParsedCommand::Unknown {
cmd: "echo hello world".into(),
}],
@@ -2253,7 +2256,8 @@ mod tests {
turn_id: "turn-1".into(),
completed_at_ms: 0,
command: vec!["ls".into()],
cwd: test_path_buf("/tmp").abs(),
cwd: PathUri::from_abs_path(&test_path_buf("/tmp").abs()),
path_convention: PathConvention::native(),
parsed_cmd: vec![ParsedCommand::Unknown { cmd: "ls".into() }],
source: ExecCommandSource::Agent,
interaction_input: None,
@@ -2519,7 +2523,8 @@ mod tests {
turn_id: "turn-a".into(),
completed_at_ms: 0,
command: vec!["echo".into(), "done".into()],
cwd: test_path_buf("/tmp").abs(),
cwd: PathUri::from_abs_path(&test_path_buf("/tmp").abs()),
path_convention: PathConvention::native(),
parsed_cmd: vec![ParsedCommand::Unknown {
cmd: "echo done".into(),
}],
@@ -2617,7 +2622,8 @@ mod tests {
turn_id: "turn-missing".into(),
completed_at_ms: 0,
command: vec!["echo".into(), "done".into()],
cwd: test_path_buf("/tmp").abs(),
cwd: PathUri::from_abs_path(&test_path_buf("/tmp").abs()),
path_convention: PathConvention::native(),
parsed_cmd: vec![ParsedCommand::Unknown {
cmd: "echo done".into(),
}],