protocol: retain executor cwd in command events

This commit is contained in:
Adam Perry
2026-06-13 03:33:29 +00:00
parent 385e9e546a
commit 7d8a9ef36b
13 changed files with 275 additions and 85 deletions

View File

@@ -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;

View File

@@ -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,
}
);
}

View File

@@ -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(),
}],

View File

@@ -6,6 +6,8 @@ use codex_async_utils::CancelErr;
use codex_async_utils::OrCancelExt;
use codex_network_proxy::PROXY_ACTIVE_ENV_KEY;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathConvention;
use codex_utils_path_uri::PathUri;
use tokio_util::sync::CancellationToken;
use tracing::error;
use uuid::Uuid;
@@ -150,6 +152,7 @@ pub(crate) async fn execute_user_shell_command(
let raw_command = command;
#[allow(deprecated)]
let cwd = turn_context.cwd.clone();
let cwd_uri = PathUri::from_abs_path(&cwd);
let parsed_cmd = parse_command(&display_command);
session
@@ -161,7 +164,8 @@ pub(crate) async fn execute_user_shell_command(
turn_id: turn_context.sub_id.clone(),
started_at_ms: now_unix_timestamp_ms(),
command: display_command.clone(),
cwd: cwd.clone(),
cwd: cwd_uri.clone(),
path_convention: PathConvention::native(),
parsed_cmd: parsed_cmd.clone(),
source: ExecCommandSource::UserShell,
interaction_input: None,
@@ -235,7 +239,8 @@ pub(crate) async fn execute_user_shell_command(
turn_id: turn_context.sub_id.clone(),
completed_at_ms: now_unix_timestamp_ms(),
command: display_command.clone(),
cwd: cwd.clone(),
cwd: cwd_uri.clone(),
path_convention: PathConvention::native(),
parsed_cmd: parsed_cmd.clone(),
source: ExecCommandSource::UserShell,
interaction_input: None,
@@ -260,7 +265,8 @@ pub(crate) async fn execute_user_shell_command(
turn_id: turn_context.sub_id.clone(),
completed_at_ms: now_unix_timestamp_ms(),
command: display_command.clone(),
cwd: cwd.clone(),
cwd: cwd_uri.clone(),
path_convention: PathConvention::native(),
parsed_cmd: parsed_cmd.clone(),
source: ExecCommandSource::UserShell,
interaction_input: None,
@@ -305,7 +311,8 @@ pub(crate) async fn execute_user_shell_command(
turn_id: turn_context.sub_id.clone(),
completed_at_ms: now_unix_timestamp_ms(),
command: display_command,
cwd,
cwd: cwd_uri,
path_convention: PathConvention::native(),
parsed_cmd,
source: ExecCommandSource::UserShell,
interaction_input: None,

View File

@@ -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::PathConvention;
use codex_utils_path_uri::PathUri;
use std::collections::HashMap;
use std::path::PathBuf;
use std::time::Duration;
@@ -92,28 +94,21 @@ fn tracker_update_for_known_delta<'a>(
}
}
pub(crate) async fn emit_exec_command_begin(
ctx: ToolEventCtx<'_>,
command: &[String],
cwd: &AbsolutePathBuf,
parsed_cmd: &[ParsedCommand],
source: ExecCommandSource,
interaction_input: Option<String>,
process_id: Option<&str>,
) {
async fn emit_exec_command_begin(ctx: ToolEventCtx<'_>, exec_input: ExecCommandInput<'_>) {
ctx.session
.send_event(
ctx.turn,
EventMsg::ExecCommandBegin(ExecCommandBeginEvent {
call_id: ctx.call_id.to_string(),
process_id: process_id.map(str::to_owned),
process_id: exec_input.process_id.map(str::to_owned),
turn_id: ctx.turn.sub_id.clone(),
started_at_ms: now_unix_timestamp_ms(),
command: command.to_vec(),
cwd: cwd.clone(),
parsed_cmd: parsed_cmd.to_vec(),
source,
interaction_input,
command: exec_input.command.to_vec(),
cwd: exec_input.cwd.clone(),
path_convention: exec_input.path_convention,
parsed_cmd: exec_input.parsed_cmd.to_vec(),
source: exec_input.source,
interaction_input: exec_input.interaction_input.map(str::to_owned),
}),
)
.await;
@@ -122,7 +117,8 @@ pub(crate) async fn emit_exec_command_begin(
pub(crate) enum ToolEmitter {
Shell {
command: Vec<String>,
cwd: AbsolutePathBuf,
cwd: PathUri,
path_convention: PathConvention,
source: ExecCommandSource,
parsed_cmd: Vec<ParsedCommand>,
},
@@ -133,7 +129,8 @@ pub(crate) enum ToolEmitter {
},
UnifiedExec {
command: Vec<String>,
cwd: AbsolutePathBuf,
cwd: PathUri,
path_convention: PathConvention,
source: ExecCommandSource,
parsed_cmd: Vec<ParsedCommand>,
process_id: Option<String>,
@@ -145,7 +142,8 @@ impl ToolEmitter {
let parsed_cmd = parse_command(&command);
Self::Shell {
command,
cwd,
cwd: PathUri::from_abs_path(&cwd),
path_convention: PathConvention::native(),
source,
parsed_cmd,
}
@@ -165,7 +163,8 @@ impl ToolEmitter {
pub fn unified_exec(
command: &[String],
cwd: AbsolutePathBuf,
cwd: PathUri,
path_convention: PathConvention,
source: ExecCommandSource,
process_id: Option<String>,
) -> Self {
@@ -173,6 +172,7 @@ impl ToolEmitter {
Self::UnifiedExec {
command: command.to_vec(),
cwd,
path_convention,
source,
parsed_cmd,
process_id,
@@ -185,6 +185,7 @@ impl ToolEmitter {
Self::Shell {
command,
cwd,
path_convention,
source,
parsed_cmd,
..
@@ -194,7 +195,12 @@ impl ToolEmitter {
emit_exec_stage(
ctx,
ExecCommandInput::new(
command, cwd, parsed_cmd, *source, /*interaction_input*/ None,
command,
cwd,
*path_convention,
parsed_cmd,
*source,
/*interaction_input*/ None,
/*process_id*/ None,
),
stage,
@@ -314,6 +320,7 @@ impl ToolEmitter {
Self::UnifiedExec {
command,
cwd,
path_convention,
source,
parsed_cmd,
process_id,
@@ -325,6 +332,7 @@ impl ToolEmitter {
ExecCommandInput::new(
command,
cwd,
*path_convention,
parsed_cmd,
*source,
/*interaction_input*/ None,
@@ -432,7 +440,8 @@ impl ToolEmitter {
struct ExecCommandInput<'a> {
command: &'a [String],
cwd: &'a AbsolutePathBuf,
cwd: &'a PathUri,
path_convention: PathConvention,
parsed_cmd: &'a [ParsedCommand],
source: ExecCommandSource,
interaction_input: Option<&'a str>,
@@ -442,7 +451,8 @@ struct ExecCommandInput<'a> {
impl<'a> ExecCommandInput<'a> {
fn new(
command: &'a [String],
cwd: &'a AbsolutePathBuf,
cwd: &'a PathUri,
path_convention: PathConvention,
parsed_cmd: &'a [ParsedCommand],
source: ExecCommandSource,
interaction_input: Option<&'a str>,
@@ -451,6 +461,7 @@ impl<'a> ExecCommandInput<'a> {
Self {
command,
cwd,
path_convention,
parsed_cmd,
source,
interaction_input,
@@ -476,16 +487,7 @@ async fn emit_exec_stage(
) {
match stage {
ToolEventStage::Begin => {
emit_exec_command_begin(
ctx,
exec_input.command,
exec_input.cwd,
exec_input.parsed_cmd,
exec_input.source,
exec_input.interaction_input.map(str::to_owned),
exec_input.process_id,
)
.await;
emit_exec_command_begin(ctx, exec_input).await;
}
ToolEventStage::Success { output, .. }
| ToolEventStage::Failure(ToolEventFailure::Output(output)) => {
@@ -548,6 +550,7 @@ async fn emit_exec_end(
completed_at_ms: now_unix_timestamp_ms(),
command: exec_input.command.to_vec(),
cwd: exec_input.cwd.clone(),
path_convention: exec_input.path_convention,
parsed_cmd: exec_input.parsed_cmd.to_vec(),
source: exec_input.source,
interaction_input: exec_input.interaction_input.map(str::to_owned),

View File

@@ -237,6 +237,7 @@ impl ExecCommandHandler {
max_output_tokens,
cwd,
cwd_uri,
path_convention: turn_environment.path_convention(),
sandbox_cwd,
environment,
shell_mode,
@@ -350,6 +351,7 @@ impl ExecCommandHandler {
max_output_tokens,
cwd,
cwd_uri,
path_convention: turn_environment.path_convention(),
sandbox_cwd,
environment,
shell_mode,

View File

@@ -22,7 +22,8 @@ use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::ExecCommandOutputDeltaEvent;
use codex_protocol::protocol::ExecCommandSource;
use codex_protocol::protocol::ExecOutputStream;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathConvention;
use codex_utils_path_uri::PathUri;
pub(crate) const TRAILING_OUTPUT_GRACE: Duration = Duration::from_millis(100);
@@ -110,7 +111,8 @@ pub(crate) fn spawn_exit_watcher(
turn_ref: Arc<TurnContext>,
call_id: String,
command: Vec<String>,
cwd: AbsolutePathBuf,
cwd: PathUri,
path_convention: PathConvention,
process_id: i32,
transcript: Arc<Mutex<HeadTailBuffer>>,
started_at: Instant,
@@ -130,6 +132,7 @@ pub(crate) fn spawn_exit_watcher(
call_id,
command,
cwd,
path_convention,
Some(process_id.to_string()),
transcript,
String::new(),
@@ -145,6 +148,7 @@ pub(crate) fn spawn_exit_watcher(
call_id,
command,
cwd,
path_convention,
Some(process_id.to_string()),
transcript,
String::new(),
@@ -197,7 +201,8 @@ pub(crate) async fn emit_exec_end_for_unified_exec(
turn_ref: Arc<TurnContext>,
call_id: String,
command: Vec<String>,
cwd: AbsolutePathBuf,
cwd: PathUri,
path_convention: PathConvention,
process_id: Option<String>,
transcript: Arc<Mutex<HeadTailBuffer>>,
fallback_output: String,
@@ -222,6 +227,7 @@ pub(crate) async fn emit_exec_end_for_unified_exec(
let emitter = ToolEmitter::unified_exec(
&command,
cwd,
path_convention,
ExecCommandSource::UnifiedExecStartup,
process_id,
);
@@ -242,7 +248,8 @@ pub(crate) async fn emit_failed_exec_end_for_unified_exec(
turn_ref: Arc<TurnContext>,
call_id: String,
command: Vec<String>,
cwd: AbsolutePathBuf,
cwd: PathUri,
path_convention: PathConvention,
process_id: Option<String>,
transcript: Arc<Mutex<HeadTailBuffer>>,
fallback_output: String,
@@ -276,6 +283,7 @@ pub(crate) async fn emit_failed_exec_end_for_unified_exec(
let emitter = ToolEmitter::unified_exec(
&command,
cwd,
path_convention,
ExecCommandSource::UnifiedExecStartup,
process_id,
);

View File

@@ -33,6 +33,7 @@ use codex_protocol::models::AdditionalPermissionProfile;
use codex_tools::UnifiedExecShellMode;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_output_truncation::TruncationPolicy;
use codex_utils_path_uri::PathConvention;
use codex_utils_path_uri::PathUri;
use rand::Rng;
use rand::rng;
@@ -97,9 +98,10 @@ pub(crate) struct ExecCommandRequest {
pub environment_id: String,
pub yield_time_ms: u64,
pub max_output_tokens: Option<usize>,
/// App-host-compatible cwd retained for approval and event compatibility.
/// App-host-compatible cwd retained for local sandbox and approval handling.
pub cwd: AbsolutePathBuf,
pub cwd_uri: PathUri,
pub path_convention: PathConvention,
pub sandbox_cwd: Option<AbsolutePathBuf>,
pub environment: Arc<Environment>,
pub shell_mode: UnifiedExecShellMode,

View File

@@ -58,6 +58,8 @@ use codex_protocol::protocol::ExecCommandSource;
use codex_tools::ToolName;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_output_truncation::approx_token_count;
use codex_utils_path_uri::PathConvention;
use codex_utils_path_uri::PathUri;
const UNIFIED_EXEC_ENV: [(&str, &str); 10] = [
("NO_COLOR", "1"),
@@ -337,7 +339,6 @@ async fn emit_failed_initial_exec_end_if_unstored(
process_started_alive: bool,
context: &UnifiedExecContext,
request: &ExecCommandRequest,
cwd: AbsolutePathBuf,
transcript: Arc<tokio::sync::Mutex<HeadTailBuffer>>,
fallback_output: String,
message: String,
@@ -352,7 +353,8 @@ async fn emit_failed_initial_exec_end_if_unstored(
Arc::clone(&context.turn),
context.call_id.clone(),
request.command.clone(),
cwd,
request.cwd_uri.clone(),
request.path_convention,
Some(request.process_id.to_string()),
transcript,
fallback_output,
@@ -459,7 +461,8 @@ impl UnifiedExecProcessManager {
);
let emitter = ToolEmitter::unified_exec(
&request.command,
cwd.clone(),
request.cwd_uri.clone(),
request.path_convention,
ExecCommandSource::UnifiedExecStartup,
Some(request.process_id.to_string()),
);
@@ -478,6 +481,8 @@ impl UnifiedExecProcessManager {
&request.command,
request.hook_command.clone(),
cwd.clone(),
request.cwd_uri.clone(),
request.path_convention,
start,
request.process_id,
request.tty,
@@ -536,7 +541,6 @@ impl UnifiedExecProcessManager {
process_started_alive,
context,
&request,
cwd.clone(),
Arc::clone(&transcript),
text.clone(),
message.clone(),
@@ -556,7 +560,6 @@ impl UnifiedExecProcessManager {
process_started_alive,
context,
&request,
cwd.clone(),
Arc::clone(&transcript),
text.clone(),
message.clone(),
@@ -608,7 +611,6 @@ impl UnifiedExecProcessManager {
process_started_alive,
context,
&request,
cwd.clone(),
Arc::clone(&transcript),
text.clone(),
message.clone(),
@@ -625,7 +627,8 @@ impl UnifiedExecProcessManager {
Arc::clone(&context.turn),
context.call_id.clone(),
request.command.clone(),
cwd.clone(),
request.cwd_uri.clone(),
request.path_convention,
Some(process_id.to_string()),
Arc::clone(&transcript),
text.clone(),
@@ -885,6 +888,8 @@ impl UnifiedExecProcessManager {
command: &[String],
hook_command: String,
cwd: AbsolutePathBuf,
cwd_uri: PathUri,
path_convention: PathConvention,
started_at: Instant,
process_id: i32,
tty: bool,
@@ -923,7 +928,8 @@ impl UnifiedExecProcessManager {
Arc::clone(&context.turn),
context.call_id.clone(),
command.to_vec(),
cwd,
cwd_uri,
path_convention,
process_id,
transcript,
started_at,

View File

@@ -296,6 +296,7 @@ async fn failed_initial_end_for_unstored_process_uses_fallback_output() {
#[allow(deprecated)]
cwd: turn.cwd.clone(),
cwd_uri: turn_environment.cwd_uri().clone(),
path_convention: turn_environment.path_convention(),
#[allow(deprecated)]
sandbox_cwd: Some(turn.cwd.clone()),
environment: turn
@@ -322,8 +323,6 @@ async fn failed_initial_end_for_unstored_process_uses_fallback_output() {
/*process_started_alive*/ false,
&context,
&request,
#[allow(deprecated)]
turn.cwd.clone(),
transcript,
"PRE_DENIAL_MARKER".to_string(),
"Network access denied".to_string(),
@@ -345,6 +344,8 @@ async fn failed_initial_end_for_unstored_process_uses_fallback_output() {
);
assert_eq!(end_event.exit_code, -1);
assert_eq!(end_event.process_id.as_deref(), Some("123"));
assert_eq!(end_event.cwd, request.cwd_uri);
assert_eq!(end_event.path_convention, request.path_convention);
assert_eq!(
end_event.aggregated_output,
"PRE_DENIAL_MARKER\nNetwork access denied"

View File

@@ -160,10 +160,7 @@ async fn windows_exec_server_records_host_shell_mismatch() -> Result<()> {
);
assert_eq!(
(begin.cwd.clone(), begin.source),
(
test.config.cwd.clone(),
ExecCommandSource::UnifiedExecStartup,
),
(selected_cwd.clone(), ExecCommandSource::UnifiedExecStartup),
);
let end = end.context("exec_command should emit an end event")?;
@@ -180,7 +177,7 @@ async fn windows_exec_server_records_host_shell_mismatch() -> Result<()> {
),
(
begin.command,
test.config.cwd.clone(),
selected_cwd,
ExecCommandSource::UnifiedExecStartup,
String::new(),
String::new(),

View File

@@ -17,6 +17,7 @@ use codex_protocol::protocol::ExecCommandSource;
use codex_protocol::protocol::ExecCommandStatus;
use codex_protocol::protocol::Op;
use codex_protocol::user_input::UserInput;
use codex_utils_path_uri::PathConvention;
use codex_utils_path_uri::PathUri;
use core_test_support::TempDirExt;
use core_test_support::assert_regex_match;
@@ -430,7 +431,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.as_path(), cwd.as_path());
assert_eq!(
(begin_event.cwd, begin_event.path_convention),
(PathUri::from_path(&cwd)?, PathConvention::native()),
);
wait_for_event(&test.codex, |event| {
matches!(event, EventMsg::TurnComplete(_))
@@ -495,8 +499,8 @@ async fn unified_exec_resolves_relative_workdir() -> Result<()> {
.await;
assert_eq!(
begin_event.cwd.as_path(),
workdir.as_path(),
(begin_event.cwd, begin_event.path_convention),
(PathUri::from_path(&workdir)?, PathConvention::native(),),
"exec_command cwd should resolve relative workdir against turn cwd",
);
@@ -558,8 +562,8 @@ async fn unified_exec_respects_workdir_override() -> Result<()> {
.await;
assert_eq!(
begin_event.cwd.as_path(),
workdir.as_path(),
(begin_event.cwd, begin_event.path_convention),
(PathUri::from_path(&workdir)?, PathConvention::native(),),
"exec_command cwd should reflect the requested workdir override"
);

View File

@@ -3206,8 +3206,11 @@ pub struct ExecCommandBeginEvent {
pub started_at_ms: i64,
/// The command to be executed.
pub command: Vec<String>,
/// The command's working directory if not the default cwd for the agent.
pub cwd: AbsolutePathBuf,
/// The command's working directory in the executing environment.
pub cwd: PathUri,
/// Path syntax used by the executing environment.
#[serde(default = "PathConvention::native")]
pub path_convention: PathConvention,
pub parsed_cmd: Vec<ParsedCommand>,
/// Where the command originated. Defaults to Agent for backward compatibility.
#[serde(default)]
@@ -3232,8 +3235,11 @@ pub struct ExecCommandEndEvent {
pub completed_at_ms: i64,
/// The command that was executed.
pub command: Vec<String>,
/// The command's working directory if not the default cwd for the agent.
pub cwd: AbsolutePathBuf,
/// The command's working directory in the executing environment.
pub cwd: PathUri,
/// Path syntax used by the executing environment.
#[serde(default = "PathConvention::native")]
pub path_convention: PathConvention,
pub parsed_cmd: Vec<ParsedCommand>,
/// Where the command originated. Defaults to Agent for backward compatibility.
#[serde(default)]
@@ -5385,6 +5391,25 @@ mod tests {
Ok(())
}
#[test]
fn deserialize_legacy_exec_command_event_uses_native_path_convention() -> Result<()> {
let cwd = AbsolutePathBuf::current_dir()?;
let value = json!({
"call_id": "call-1",
"turn_id": "turn-1",
"command": ["echo", "hello"],
"cwd": cwd,
"parsed_cmd": [],
});
let event: ExecCommandBeginEvent = serde_json::from_value(value)?;
assert_eq!(
(event.cwd, event.path_convention),
(PathUri::from_abs_path(&cwd), PathConvention::native()),
);
Ok(())
}
#[test]
fn serialize_mcp_startup_update_event() -> Result<()> {
let event = Event {