mirror of
https://github.com/openai/codex.git
synced 2026-09-05 15:18:41 +00:00
protocol: retain executor cwd in command events
This commit is contained in:
@@ -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(),
|
||||
}],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user