mirror of
https://github.com/openai/codex.git
synced 2026-10-28 18:58:46 +00:00
core: preserve command cwd path uri
This commit is contained in:
@@ -34,9 +34,24 @@ 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_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::ApiPathString;
|
||||
use codex_utils_path_uri::PathConvention;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[expect(
|
||||
clippy::expect_used,
|
||||
reason = "legacy app-server command items still require a host-native cwd"
|
||||
)]
|
||||
fn project_command_cwd_to_host(cwd: &PathUri) -> AbsolutePathBuf {
|
||||
let cwd = ApiPathString::from_path_uri(cwd, PathConvention::native())
|
||||
.expect("command cwd should render using the app-server host path convention");
|
||||
AbsolutePathBuf::try_from(cwd.into_string())
|
||||
.expect("projected command cwd should be absolute on the app-server host")
|
||||
}
|
||||
|
||||
pub fn build_file_change_approval_request_item(
|
||||
payload: &ApplyPatchApprovalRequestEvent,
|
||||
) -> ThreadItem {
|
||||
@@ -66,10 +81,11 @@ pub fn build_file_change_end_item(payload: &PatchApplyEndEvent) -> ThreadItem {
|
||||
pub fn build_command_execution_approval_request_item(
|
||||
payload: &ExecApprovalRequestEvent,
|
||||
) -> ThreadItem {
|
||||
let cwd = project_command_cwd_to_host(&payload.cwd);
|
||||
ThreadItem::CommandExecution {
|
||||
id: payload.call_id.clone(),
|
||||
command: shlex_join(&payload.command),
|
||||
cwd: payload.cwd.clone(),
|
||||
cwd: cwd.clone(),
|
||||
process_id: None,
|
||||
source: CommandExecutionSource::Agent,
|
||||
status: CommandExecutionStatus::InProgress,
|
||||
@@ -77,7 +93,7 @@ pub fn build_command_execution_approval_request_item(
|
||||
.parsed_cmd
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|parsed| CommandAction::from_core_with_cwd(parsed, &payload.cwd))
|
||||
.map(|parsed| CommandAction::from_core_with_cwd(parsed, &cwd))
|
||||
.collect(),
|
||||
aggregated_output: None,
|
||||
exit_code: None,
|
||||
@@ -86,10 +102,11 @@ pub fn build_command_execution_approval_request_item(
|
||||
}
|
||||
|
||||
pub fn build_command_execution_begin_item(payload: &ExecCommandBeginEvent) -> ThreadItem {
|
||||
let cwd = project_command_cwd_to_host(&payload.cwd);
|
||||
ThreadItem::CommandExecution {
|
||||
id: payload.call_id.clone(),
|
||||
command: shlex_join(&payload.command),
|
||||
cwd: payload.cwd.clone(),
|
||||
cwd: cwd.clone(),
|
||||
process_id: payload.process_id.clone(),
|
||||
source: payload.source.into(),
|
||||
status: CommandExecutionStatus::InProgress,
|
||||
@@ -97,7 +114,7 @@ pub fn build_command_execution_begin_item(payload: &ExecCommandBeginEvent) -> Th
|
||||
.parsed_cmd
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|parsed| CommandAction::from_core_with_cwd(parsed, &payload.cwd))
|
||||
.map(|parsed| CommandAction::from_core_with_cwd(parsed, &cwd))
|
||||
.collect(),
|
||||
aggregated_output: None,
|
||||
exit_code: None,
|
||||
@@ -112,11 +129,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 cwd = project_command_cwd_to_host(&payload.cwd);
|
||||
|
||||
ThreadItem::CommandExecution {
|
||||
id: payload.call_id.clone(),
|
||||
command: shlex_join(&payload.command),
|
||||
cwd: payload.cwd.clone(),
|
||||
cwd: cwd.clone(),
|
||||
process_id: payload.process_id.clone(),
|
||||
source: payload.source.into(),
|
||||
status: (&payload.status).into(),
|
||||
@@ -124,7 +142,7 @@ pub fn build_command_execution_end_item(payload: &ExecCommandEndEvent) -> Thread
|
||||
.parsed_cmd
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|parsed| CommandAction::from_core_with_cwd(parsed, &payload.cwd))
|
||||
.map(|parsed| CommandAction::from_core_with_cwd(parsed, &cwd))
|
||||
.collect(),
|
||||
aggregated_output,
|
||||
exit_code: Some(payload.exit_code),
|
||||
|
||||
@@ -1278,6 +1278,7 @@ 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::PathUri;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
@@ -2072,7 +2073,7 @@ 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()),
|
||||
parsed_cmd: vec![ParsedCommand::Unknown {
|
||||
cmd: "echo hello world".into(),
|
||||
}],
|
||||
@@ -2312,7 +2313,7 @@ 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()),
|
||||
parsed_cmd: vec![ParsedCommand::Unknown { cmd: "ls".into() }],
|
||||
source: ExecCommandSource::Agent,
|
||||
interaction_input: None,
|
||||
@@ -2578,7 +2579,7 @@ 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()),
|
||||
parsed_cmd: vec![ParsedCommand::Unknown {
|
||||
cmd: "echo done".into(),
|
||||
}],
|
||||
@@ -2676,7 +2677,7 @@ 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()),
|
||||
parsed_cmd: vec![ParsedCommand::Unknown {
|
||||
cmd: "echo done".into(),
|
||||
}],
|
||||
|
||||
@@ -113,6 +113,8 @@ use codex_protocol::request_user_input::RequestUserInputResponse as CoreRequestU
|
||||
use codex_sandboxing::policy_transforms::intersect_permission_profiles;
|
||||
use codex_shell_command::parse_command::shlex_join;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::ApiPathString;
|
||||
use codex_utils_path_uri::PathConvention;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::SystemTime;
|
||||
@@ -561,6 +563,16 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
parsed_cmd,
|
||||
..
|
||||
} = ev;
|
||||
#[expect(
|
||||
clippy::expect_used,
|
||||
reason = "legacy command approval payloads still require a host-native cwd"
|
||||
)]
|
||||
let cwd = {
|
||||
let cwd = ApiPathString::from_path_uri(&cwd, PathConvention::native())
|
||||
.expect("command cwd should render using the app-server host path convention");
|
||||
AbsolutePathBuf::try_from(cwd.into_string())
|
||||
.expect("projected command cwd should be absolute on the app-server host")
|
||||
};
|
||||
let command_actions = parsed_cmd
|
||||
.iter()
|
||||
.cloned()
|
||||
|
||||
@@ -25,6 +25,9 @@ use codex_protocol::request_permissions::RequestPermissionsResponse;
|
||||
use codex_protocol::request_user_input::RequestUserInputArgs;
|
||||
use codex_protocol::request_user_input::RequestUserInputResponse;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::ApiPathString;
|
||||
use codex_utils_path_uri::PathConvention;
|
||||
use serde_json::Value;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::Mutex;
|
||||
@@ -469,7 +472,33 @@ async fn handle_exec_approval(
|
||||
available_decisions,
|
||||
..
|
||||
} = event;
|
||||
let decision = if routes_approval_to_guardian(parent_ctx) {
|
||||
let routes_to_guardian = routes_approval_to_guardian(parent_ctx);
|
||||
let guardian_cwd = if routes_to_guardian {
|
||||
match ApiPathString::from_path_uri(&cwd, PathConvention::native()) {
|
||||
Ok(rendered_cwd) => match AbsolutePathBuf::try_from(rendered_cwd.into_string()) {
|
||||
Ok(cwd) => Some(cwd),
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
cwd = %cwd,
|
||||
%err,
|
||||
"delegated command approval cwd could not be projected onto the Codex host for guardian review"
|
||||
);
|
||||
None
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
cwd = %cwd,
|
||||
%err,
|
||||
"delegated command approval cwd could not be projected onto the Codex host for guardian review"
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let decision = if let Some(cwd) = guardian_cwd {
|
||||
let review_cancel = cancel_token.child_token();
|
||||
let review_rx = spawn_approval_request_review(
|
||||
Arc::clone(parent_session),
|
||||
|
||||
@@ -324,7 +324,7 @@ async fn handle_exec_approval_uses_call_id_for_guardian_review_and_approval_id_f
|
||||
turn_id: "child-turn-1".to_string(),
|
||||
started_at_ms: 0,
|
||||
command: vec!["rm".to_string(), "-rf".to_string(), "tmp".to_string()],
|
||||
cwd: test_path_buf("/tmp").abs(),
|
||||
cwd: codex_utils_path_uri::PathUri::from_abs_path(&test_path_buf("/tmp").abs()),
|
||||
reason: Some("unsafe subcommand".to_string()),
|
||||
network_approval_context: None,
|
||||
proposed_execpolicy_amendment: None,
|
||||
|
||||
@@ -2036,7 +2036,7 @@ impl Session {
|
||||
call_id: String,
|
||||
approval_id: Option<String>,
|
||||
command: Vec<String>,
|
||||
cwd: AbsolutePathBuf,
|
||||
cwd: PathUri,
|
||||
reason: Option<String>,
|
||||
network_approval_context: Option<NetworkApprovalContext>,
|
||||
proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
|
||||
|
||||
@@ -162,9 +162,8 @@ pub(crate) async fn execute_user_shell_command(
|
||||
|
||||
let call_id = Uuid::new_v4().to_string();
|
||||
let raw_command = command;
|
||||
// TODO(anp): Migrate user-shell events and execution plumbing to PathUri so this local-only
|
||||
// feature does not need to project the selected environment cwd onto the Codex host.
|
||||
let Ok(cwd) = turn_environment.cwd().to_abs_path() else {
|
||||
let cwd_uri = turn_environment.cwd().clone();
|
||||
let Ok(cwd) = cwd_uri.to_abs_path() else {
|
||||
send_user_shell_error(
|
||||
&session,
|
||||
turn_context.as_ref(),
|
||||
@@ -184,7 +183,7 @@ 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(),
|
||||
parsed_cmd: parsed_cmd.clone(),
|
||||
source: ExecCommandSource::UserShell,
|
||||
interaction_input: None,
|
||||
@@ -258,7 +257,7 @@ 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(),
|
||||
parsed_cmd: parsed_cmd.clone(),
|
||||
source: ExecCommandSource::UserShell,
|
||||
interaction_input: None,
|
||||
@@ -283,7 +282,7 @@ 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(),
|
||||
parsed_cmd: parsed_cmd.clone(),
|
||||
source: ExecCommandSource::UserShell,
|
||||
interaction_input: None,
|
||||
@@ -328,7 +327,7 @@ 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,
|
||||
parsed_cmd,
|
||||
source: ExecCommandSource::UserShell,
|
||||
interaction_input: None,
|
||||
|
||||
@@ -21,6 +21,7 @@ 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::PathUri;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
@@ -95,7 +96,7 @@ fn tracker_update_for_known_delta<'a>(
|
||||
pub(crate) async fn emit_exec_command_begin(
|
||||
ctx: ToolEventCtx<'_>,
|
||||
command: &[String],
|
||||
cwd: &AbsolutePathBuf,
|
||||
cwd: &PathUri,
|
||||
parsed_cmd: &[ParsedCommand],
|
||||
source: ExecCommandSource,
|
||||
interaction_input: Option<String>,
|
||||
@@ -122,7 +123,7 @@ pub(crate) async fn emit_exec_command_begin(
|
||||
pub(crate) enum ToolEmitter {
|
||||
Shell {
|
||||
command: Vec<String>,
|
||||
cwd: AbsolutePathBuf,
|
||||
cwd: PathUri,
|
||||
source: ExecCommandSource,
|
||||
parsed_cmd: Vec<ParsedCommand>,
|
||||
},
|
||||
@@ -133,7 +134,7 @@ pub(crate) enum ToolEmitter {
|
||||
},
|
||||
UnifiedExec {
|
||||
command: Vec<String>,
|
||||
cwd: AbsolutePathBuf,
|
||||
cwd: PathUri,
|
||||
source: ExecCommandSource,
|
||||
parsed_cmd: Vec<ParsedCommand>,
|
||||
process_id: Option<String>,
|
||||
@@ -145,7 +146,7 @@ impl ToolEmitter {
|
||||
let parsed_cmd = parse_command(&command);
|
||||
Self::Shell {
|
||||
command,
|
||||
cwd,
|
||||
cwd: PathUri::from_abs_path(&cwd),
|
||||
source,
|
||||
parsed_cmd,
|
||||
}
|
||||
@@ -165,7 +166,7 @@ impl ToolEmitter {
|
||||
|
||||
pub fn unified_exec(
|
||||
command: &[String],
|
||||
cwd: AbsolutePathBuf,
|
||||
cwd: PathUri,
|
||||
source: ExecCommandSource,
|
||||
process_id: Option<String>,
|
||||
) -> Self {
|
||||
@@ -432,7 +433,7 @@ impl ToolEmitter {
|
||||
|
||||
struct ExecCommandInput<'a> {
|
||||
command: &'a [String],
|
||||
cwd: &'a AbsolutePathBuf,
|
||||
cwd: &'a PathUri,
|
||||
parsed_cmd: &'a [ParsedCommand],
|
||||
source: ExecCommandSource,
|
||||
interaction_input: Option<&'a str>,
|
||||
@@ -442,7 +443,7 @@ struct ExecCommandInput<'a> {
|
||||
impl<'a> ExecCommandInput<'a> {
|
||||
fn new(
|
||||
command: &'a [String],
|
||||
cwd: &'a AbsolutePathBuf,
|
||||
cwd: &'a PathUri,
|
||||
parsed_cmd: &'a [ParsedCommand],
|
||||
source: ExecCommandSource,
|
||||
interaction_input: Option<&'a str>,
|
||||
|
||||
@@ -128,7 +128,7 @@ impl ExecCommandHandler {
|
||||
"unified exec is unavailable in this session".to_string(),
|
||||
));
|
||||
};
|
||||
let cwd = environment_args
|
||||
let cwd_uri = environment_args
|
||||
.workdir
|
||||
.as_ref()
|
||||
.filter(|workdir| !workdir.is_empty())
|
||||
@@ -136,9 +136,9 @@ impl ExecCommandHandler {
|
||||
.unwrap_or_else(|| turn_environment.cwd().clone());
|
||||
// TODO(anp): Migrate command approvals, granted permissions, and tool hooks to PathUri so
|
||||
// foreign command working directories do not require a host-native projection here.
|
||||
let native_cwd = cwd.to_abs_path().map_err(|err| {
|
||||
let native_cwd = cwd_uri.to_abs_path().map_err(|err| {
|
||||
FunctionCallError::RespondToModel(format!(
|
||||
"command workdir `{cwd}` is not native to the Codex host: {err}"
|
||||
"command workdir `{cwd_uri}` is not native to the Codex host: {err}"
|
||||
))
|
||||
})?;
|
||||
let environment = Arc::clone(&turn_environment.environment);
|
||||
@@ -280,7 +280,8 @@ impl ExecCommandHandler {
|
||||
process_id,
|
||||
yield_time_ms,
|
||||
max_output_tokens,
|
||||
cwd,
|
||||
cwd_uri,
|
||||
cwd: native_cwd,
|
||||
// TODO(anp): Migrate the sandbox orchestration traits to PathUri so the
|
||||
// selected environment root can remain foreign until sandbox enforcement.
|
||||
sandbox_cwd: turn_environment.cwd().to_abs_path().map_err(|err| {
|
||||
|
||||
@@ -526,7 +526,7 @@ impl NetworkApprovalService {
|
||||
/*approval_id*/ None,
|
||||
prompt_command,
|
||||
#[allow(deprecated)]
|
||||
turn_context.cwd.clone(),
|
||||
codex_utils_path_uri::PathUri::from_abs_path(&turn_context.cwd),
|
||||
Some(prompt_reason),
|
||||
Some(network_approval_context.clone()),
|
||||
/*proposed_execpolicy_amendment*/ None,
|
||||
|
||||
@@ -176,7 +176,7 @@ impl Approvable<ShellRequest> for ShellRuntime {
|
||||
call_id,
|
||||
/*approval_id*/ None,
|
||||
command,
|
||||
cwd,
|
||||
codex_utils_path_uri::PathUri::from_abs_path(&cwd),
|
||||
reason,
|
||||
ctx.network_approval_context.clone(),
|
||||
req.exec_approval_requirement
|
||||
|
||||
@@ -505,7 +505,7 @@ impl CoreShellActionProvider {
|
||||
call_id,
|
||||
approval_id,
|
||||
command,
|
||||
workdir.clone(),
|
||||
codex_utils_path_uri::PathUri::from_abs_path(&workdir),
|
||||
/*reason*/ None,
|
||||
/*network_approval_context*/ None,
|
||||
/*proposed_execpolicy_amendment*/ None,
|
||||
|
||||
@@ -64,7 +64,8 @@ pub struct UnifiedExecRequest {
|
||||
pub shell_type: ShellType,
|
||||
pub hook_command: String,
|
||||
pub process_id: i32,
|
||||
pub cwd: PathUri,
|
||||
pub cwd_uri: PathUri,
|
||||
pub cwd: AbsolutePathBuf,
|
||||
pub sandbox_cwd: AbsolutePathBuf,
|
||||
pub turn_environment: TurnEnvironment,
|
||||
pub env: HashMap<String, String>,
|
||||
@@ -137,7 +138,7 @@ impl Approvable<UnifiedExecRequest> for UnifiedExecRuntime<'_> {
|
||||
fn approval_keys(&self, req: &UnifiedExecRequest) -> Vec<Self::ApprovalKey> {
|
||||
vec![UnifiedExecApprovalKey {
|
||||
command: canonicalize_command_for_approval(&req.command),
|
||||
cwd: req.cwd.clone(),
|
||||
cwd: req.cwd_uri.clone(),
|
||||
tty: req.tty,
|
||||
sandbox_permissions: req.sandbox_permissions,
|
||||
additional_permissions: req.additional_permissions.clone(),
|
||||
@@ -154,18 +155,12 @@ impl Approvable<UnifiedExecRequest> for UnifiedExecRuntime<'_> {
|
||||
let turn = ctx.turn;
|
||||
let call_id = ctx.call_id.to_string();
|
||||
let command = req.command.clone();
|
||||
let cwd = req.cwd.clone();
|
||||
let cwd_uri = req.cwd_uri.clone();
|
||||
let retry_reason = ctx.retry_reason.clone();
|
||||
let reason = retry_reason.clone().or_else(|| req.justification.clone());
|
||||
let guardian_review_id = ctx.guardian_review_id.clone();
|
||||
Box::pin(async move {
|
||||
// TODO(anp): Migrate command approval payloads to PathUri so approvals can represent
|
||||
// working directories native to a remote environment.
|
||||
let cwd = req.cwd.to_abs_path().map_err(|err| {
|
||||
ToolError::Rejected(format!(
|
||||
"command cwd `{}` is not native to the Codex host: {err}",
|
||||
req.cwd
|
||||
))
|
||||
})?;
|
||||
if let Some(review_id) = guardian_review_id {
|
||||
return Ok(review_approval_request(
|
||||
session,
|
||||
@@ -193,7 +188,7 @@ impl Approvable<UnifiedExecRequest> for UnifiedExecRuntime<'_> {
|
||||
call_id,
|
||||
/*approval_id*/ None,
|
||||
command,
|
||||
cwd.clone(),
|
||||
cwd_uri,
|
||||
reason,
|
||||
ctx.network_approval_context.clone(),
|
||||
req.exec_approval_requirement
|
||||
@@ -248,9 +243,6 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
|
||||
);
|
||||
let network =
|
||||
managed_network_for_sandbox_permissions(req.network.as_ref(), sandbox_permissions)?;
|
||||
// TODO(anp): Migrate guardian network approval triggers to PathUri so remote working
|
||||
// directories do not suppress managed-network approval context.
|
||||
let cwd = req.cwd.to_abs_path().ok()?;
|
||||
Some(NetworkApprovalSpec {
|
||||
network: Some(network.clone()),
|
||||
mode: NetworkApprovalMode::Deferred,
|
||||
@@ -258,7 +250,7 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
|
||||
call_id: ctx.call_id.clone(),
|
||||
tool_name: flat_tool_name(&ctx.tool_name).into_owned(),
|
||||
command: req.command.clone(),
|
||||
cwd,
|
||||
cwd: req.cwd.clone(),
|
||||
sandbox_permissions: req.sandbox_permissions,
|
||||
additional_permissions: req.additional_permissions.clone(),
|
||||
justification: req.justification.clone(),
|
||||
@@ -281,7 +273,7 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
|
||||
.shell
|
||||
.as_ref()
|
||||
.unwrap_or(session_shell.as_ref());
|
||||
let shell_snapshot_location = req.turn_environment.shell_snapshot(&req.cwd);
|
||||
let shell_snapshot_location = req.turn_environment.shell_snapshot(&req.cwd_uri);
|
||||
let (file_system_sandbox_policy, _) = attempt.permissions.to_runtime_permissions();
|
||||
let launch_sandbox_permissions = sandbox_permissions_preserving_denied_reads(
|
||||
req.sandbox_permissions,
|
||||
@@ -342,8 +334,12 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
|
||||
};
|
||||
|
||||
if let UnifiedExecShellMode::ZshFork(zsh_fork_config) = &self.shell_mode {
|
||||
let command =
|
||||
build_sandbox_command(&command, &req.cwd, &env, req.additional_permissions.clone())
|
||||
let command = build_sandbox_command(
|
||||
&command,
|
||||
&req.cwd_uri,
|
||||
&env,
|
||||
req.additional_permissions.clone(),
|
||||
)
|
||||
.map_err(|error| match error {
|
||||
ToolError::Rejected(_) => {
|
||||
ToolError::Rejected("missing command line for PTY".to_string())
|
||||
@@ -376,6 +372,7 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
|
||||
.open_session_with_exec_env(
|
||||
req.process_id,
|
||||
&prepared.exec_request,
|
||||
&prepared.exec_request.cwd,
|
||||
req.tty,
|
||||
prepared.spawn_lifecycle,
|
||||
req.turn_environment.environment.as_ref(),
|
||||
@@ -398,8 +395,12 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
|
||||
}
|
||||
}
|
||||
}
|
||||
let command =
|
||||
build_sandbox_command(&command, &req.cwd, &env, req.additional_permissions.clone())
|
||||
let command = build_sandbox_command(
|
||||
&command,
|
||||
&req.cwd_uri,
|
||||
&env,
|
||||
req.additional_permissions.clone(),
|
||||
)
|
||||
.map_err(|error| match error {
|
||||
ToolError::Rejected(_) => {
|
||||
ToolError::Rejected("missing command line for PTY".to_string())
|
||||
@@ -412,12 +413,13 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
|
||||
.map_err(ToolError::Codex)?;
|
||||
exec_env.exec_server_env_config = req.exec_server_env_config.clone();
|
||||
if req.turn_environment.environment.is_remote() {
|
||||
exec_env.cwd = req.cwd.clone();
|
||||
exec_env.cwd = req.cwd_uri.clone();
|
||||
}
|
||||
self.manager
|
||||
.open_session_with_exec_env(
|
||||
req.process_id,
|
||||
&exec_env,
|
||||
&req.cwd_uri,
|
||||
req.tty,
|
||||
Box::new(NoopSpawnLifecycle),
|
||||
req.turn_environment.environment.as_ref(),
|
||||
@@ -493,7 +495,8 @@ mod tests {
|
||||
shell_type: ShellType::Sh,
|
||||
hook_command: "pwd".to_string(),
|
||||
process_id: 1000,
|
||||
cwd: PathUri::from_abs_path(&cwd),
|
||||
cwd_uri: PathUri::from_abs_path(&cwd),
|
||||
cwd,
|
||||
sandbox_cwd: sandbox_cwd.clone(),
|
||||
turn_environment: test_turn_environment(sandbox_cwd.clone()),
|
||||
env: HashMap::new(),
|
||||
@@ -592,7 +595,8 @@ mod tests {
|
||||
shell_type: ShellType::Zsh,
|
||||
hook_command: "echo hi".to_string(),
|
||||
process_id: 1000,
|
||||
cwd: PathUri::from_abs_path(&cwd),
|
||||
cwd_uri: PathUri::from_abs_path(&cwd),
|
||||
cwd: cwd.clone(),
|
||||
sandbox_cwd: cwd.clone(),
|
||||
turn_environment: test_turn_environment(cwd),
|
||||
env: HashMap::new(),
|
||||
|
||||
@@ -22,7 +22,7 @@ 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::PathUri;
|
||||
|
||||
pub(crate) const TRAILING_OUTPUT_GRACE: Duration = Duration::from_millis(100);
|
||||
|
||||
@@ -110,7 +110,7 @@ pub(crate) fn spawn_exit_watcher(
|
||||
turn_ref: Arc<TurnContext>,
|
||||
call_id: String,
|
||||
command: Vec<String>,
|
||||
cwd: AbsolutePathBuf,
|
||||
cwd: PathUri,
|
||||
process_id: i32,
|
||||
transcript: Arc<Mutex<HeadTailBuffer>>,
|
||||
started_at: Instant,
|
||||
@@ -197,7 +197,7 @@ pub(crate) async fn emit_exec_end_for_unified_exec(
|
||||
turn_ref: Arc<TurnContext>,
|
||||
call_id: String,
|
||||
command: Vec<String>,
|
||||
cwd: AbsolutePathBuf,
|
||||
cwd: PathUri,
|
||||
process_id: Option<String>,
|
||||
transcript: Arc<Mutex<HeadTailBuffer>>,
|
||||
fallback_output: String,
|
||||
@@ -242,7 +242,7 @@ 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,
|
||||
process_id: Option<String>,
|
||||
transcript: Arc<Mutex<HeadTailBuffer>>,
|
||||
fallback_output: String,
|
||||
|
||||
@@ -97,7 +97,11 @@ pub(crate) struct ExecCommandRequest {
|
||||
pub process_id: i32,
|
||||
pub yield_time_ms: u64,
|
||||
pub max_output_tokens: Option<usize>,
|
||||
pub cwd: PathUri,
|
||||
/// Environment-owned cwd retained for remote launch and emitted events.
|
||||
pub cwd_uri: PathUri,
|
||||
/// Host-native projection used only by approval, policy, and sandbox code
|
||||
/// that has not migrated to foreign paths yet.
|
||||
pub cwd: AbsolutePathBuf,
|
||||
pub sandbox_cwd: AbsolutePathBuf,
|
||||
pub turn_environment: TurnEnvironment,
|
||||
pub shell_mode: UnifiedExecShellMode,
|
||||
|
||||
@@ -103,12 +103,14 @@ async fn exec_command_with_tty(
|
||||
.map_or_else(|| turn.cwd.clone(), |workdir| turn.cwd.join(workdir));
|
||||
let command = vec!["bash".to_string(), "-lc".to_string(), cmd.to_string()];
|
||||
let request = test_exec_request(turn, command.clone(), cwd.clone(), shell_env());
|
||||
let cwd_uri = request.cwd.clone();
|
||||
|
||||
let process = Arc::new(
|
||||
manager
|
||||
.open_session_with_exec_env(
|
||||
process_id,
|
||||
&request,
|
||||
&cwd_uri,
|
||||
tty,
|
||||
Box::new(NoopSpawnLifecycle),
|
||||
turn.environments
|
||||
@@ -808,10 +810,12 @@ async fn completed_pipe_commands_preserve_exit_code() -> anyhow::Result<()> {
|
||||
);
|
||||
|
||||
let environment = codex_exec_server::Environment::default_for_tests();
|
||||
let cwd_uri = request.cwd.clone();
|
||||
let process = UnifiedExecProcessManager::default()
|
||||
.open_session_with_exec_env(
|
||||
/*process_id*/ 1234,
|
||||
&request,
|
||||
&cwd_uri,
|
||||
/*tty*/ false,
|
||||
Box::new(NoopSpawnLifecycle),
|
||||
&environment,
|
||||
@@ -850,10 +854,12 @@ async fn unified_exec_uses_remote_exec_server_when_configured() -> anyhow::Resul
|
||||
);
|
||||
|
||||
let manager = UnifiedExecProcessManager::default();
|
||||
let cwd_uri = request.cwd.clone();
|
||||
let process = manager
|
||||
.open_session_with_exec_env(
|
||||
/*process_id*/ 1234,
|
||||
&request,
|
||||
&cwd_uri,
|
||||
/*tty*/ true,
|
||||
Box::new(NoopSpawnLifecycle),
|
||||
remote_test_env.environment(),
|
||||
@@ -907,10 +913,12 @@ async fn remote_exec_server_rejects_inherited_fd_launches() -> anyhow::Result<()
|
||||
);
|
||||
|
||||
let manager = UnifiedExecProcessManager::default();
|
||||
let cwd_uri = request.cwd.clone();
|
||||
let err = manager
|
||||
.open_session_with_exec_env(
|
||||
/*process_id*/ 1234,
|
||||
&request,
|
||||
&cwd_uri,
|
||||
/*tty*/ true,
|
||||
Box::new(TestSpawnLifecycle {
|
||||
inherited_fds: vec![42],
|
||||
|
||||
@@ -57,6 +57,7 @@ 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::PathUri;
|
||||
|
||||
const UNIFIED_EXEC_ENV: [(&str, &str); 10] = [
|
||||
("NO_COLOR", "1"),
|
||||
@@ -150,13 +151,14 @@ fn exec_server_env_for_request(
|
||||
fn exec_server_params_for_request(
|
||||
process_id: i32,
|
||||
request: &ExecRequest,
|
||||
cwd: &PathUri,
|
||||
tty: bool,
|
||||
) -> codex_exec_server::ExecParams {
|
||||
let (env_policy, env) = exec_server_env_for_request(request);
|
||||
codex_exec_server::ExecParams {
|
||||
process_id: exec_server_process_id(process_id).into(),
|
||||
argv: request.command.clone(),
|
||||
cwd: request.cwd.clone(),
|
||||
cwd: cwd.clone(),
|
||||
env_policy,
|
||||
env,
|
||||
tty,
|
||||
@@ -293,7 +295,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,
|
||||
@@ -308,7 +309,7 @@ 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(),
|
||||
Some(request.process_id.to_string()),
|
||||
transcript,
|
||||
fallback_output,
|
||||
@@ -384,14 +385,8 @@ impl UnifiedExecProcessManager {
|
||||
request: ExecCommandRequest,
|
||||
context: &UnifiedExecContext,
|
||||
) -> Result<ExecCommandToolOutput, UnifiedExecError> {
|
||||
// TODO(anp): Migrate unified-exec events and retained process metadata to PathUri so
|
||||
// foreign working directories do not need a host-native projection here.
|
||||
let event_cwd = request.cwd.to_abs_path().map_err(|err| {
|
||||
UnifiedExecError::create_process(format!(
|
||||
"command cwd `{}` is not native to the Codex host: {err}",
|
||||
request.cwd
|
||||
))
|
||||
})?;
|
||||
let cwd = request.cwd.clone();
|
||||
let cwd_uri = request.cwd_uri.clone();
|
||||
let process = self.open_session_with_sandbox(&request, context).await;
|
||||
|
||||
let (process, mut deferred_network_approval) = match process {
|
||||
@@ -420,7 +415,7 @@ impl UnifiedExecProcessManager {
|
||||
);
|
||||
let emitter = ToolEmitter::unified_exec(
|
||||
&request.command,
|
||||
event_cwd.clone(),
|
||||
cwd_uri.clone(),
|
||||
ExecCommandSource::UnifiedExecStartup,
|
||||
Some(request.process_id.to_string()),
|
||||
);
|
||||
@@ -438,7 +433,8 @@ impl UnifiedExecProcessManager {
|
||||
context,
|
||||
&request.command,
|
||||
request.hook_command.clone(),
|
||||
event_cwd.clone(),
|
||||
cwd.clone(),
|
||||
cwd_uri.clone(),
|
||||
start,
|
||||
request.process_id,
|
||||
request.tty,
|
||||
@@ -497,7 +493,6 @@ impl UnifiedExecProcessManager {
|
||||
process_started_alive,
|
||||
context,
|
||||
&request,
|
||||
event_cwd.clone(),
|
||||
Arc::clone(&transcript),
|
||||
text.clone(),
|
||||
message.clone(),
|
||||
@@ -517,7 +512,6 @@ impl UnifiedExecProcessManager {
|
||||
process_started_alive,
|
||||
context,
|
||||
&request,
|
||||
event_cwd.clone(),
|
||||
Arc::clone(&transcript),
|
||||
text.clone(),
|
||||
message.clone(),
|
||||
@@ -569,7 +563,6 @@ impl UnifiedExecProcessManager {
|
||||
process_started_alive,
|
||||
context,
|
||||
&request,
|
||||
event_cwd.clone(),
|
||||
Arc::clone(&transcript),
|
||||
text.clone(),
|
||||
message.clone(),
|
||||
@@ -586,7 +579,7 @@ impl UnifiedExecProcessManager {
|
||||
Arc::clone(&context.turn),
|
||||
context.call_id.clone(),
|
||||
request.command.clone(),
|
||||
event_cwd.clone(),
|
||||
cwd_uri,
|
||||
Some(process_id.to_string()),
|
||||
Arc::clone(&transcript),
|
||||
text.clone(),
|
||||
@@ -846,6 +839,7 @@ impl UnifiedExecProcessManager {
|
||||
command: &[String],
|
||||
hook_command: String,
|
||||
cwd: AbsolutePathBuf,
|
||||
cwd_uri: PathUri,
|
||||
started_at: Instant,
|
||||
process_id: i32,
|
||||
tty: bool,
|
||||
@@ -884,7 +878,7 @@ impl UnifiedExecProcessManager {
|
||||
Arc::clone(&context.turn),
|
||||
context.call_id.clone(),
|
||||
command.to_vec(),
|
||||
cwd,
|
||||
cwd_uri,
|
||||
process_id,
|
||||
transcript,
|
||||
started_at,
|
||||
@@ -895,6 +889,7 @@ impl UnifiedExecProcessManager {
|
||||
&self,
|
||||
process_id: i32,
|
||||
request: &ExecRequest,
|
||||
cwd: &PathUri,
|
||||
tty: bool,
|
||||
mut spawn_lifecycle: SpawnLifecycleHandle,
|
||||
environment: &codex_exec_server::Environment,
|
||||
@@ -994,7 +989,9 @@ impl UnifiedExecProcessManager {
|
||||
|
||||
let started = environment
|
||||
.get_exec_backend()
|
||||
.start(exec_server_params_for_request(process_id, request, tty))
|
||||
.start(exec_server_params_for_request(
|
||||
process_id, request, cwd, tty,
|
||||
))
|
||||
.await
|
||||
.map_err(|err| UnifiedExecError::create_process(err.to_string()))?;
|
||||
spawn_lifecycle.after_spawn();
|
||||
@@ -1082,6 +1079,7 @@ impl UnifiedExecProcessManager {
|
||||
shell_type: request.shell_type,
|
||||
hook_command: request.hook_command.clone(),
|
||||
process_id: request.process_id,
|
||||
cwd_uri: request.cwd_uri.clone(),
|
||||
cwd: request.cwd.clone(),
|
||||
sandbox_cwd: request.sandbox_cwd.clone(),
|
||||
turn_environment: request.turn_environment.clone(),
|
||||
|
||||
@@ -114,11 +114,12 @@ fn exec_server_params_use_path_uri_and_env_policy_overlay_contract() {
|
||||
arg0: None,
|
||||
};
|
||||
|
||||
let cwd = PathUri::parse("file:///C:/remote%20workspace").expect("foreign cwd URI");
|
||||
let params =
|
||||
exec_server_params_for_request(/*process_id*/ 123, &request, /*tty*/ true);
|
||||
exec_server_params_for_request(/*process_id*/ 123, &request, &cwd, /*tty*/ true);
|
||||
|
||||
assert_eq!(params.process_id.as_str(), "123");
|
||||
assert_eq!(params.cwd, command_cwd);
|
||||
assert_eq!(params.cwd, cwd);
|
||||
assert!(params.env_policy.is_some());
|
||||
assert_eq!(
|
||||
params.env,
|
||||
@@ -201,8 +202,14 @@ async fn failed_initial_end_for_unstored_process_uses_fallback_output() {
|
||||
process_id: 123,
|
||||
yield_time_ms: 1000,
|
||||
max_output_tokens: None,
|
||||
cwd_uri: turn
|
||||
.environments
|
||||
.primary()
|
||||
.expect("primary environment")
|
||||
.cwd()
|
||||
.clone(),
|
||||
#[allow(deprecated)]
|
||||
cwd: PathUri::from_abs_path(&turn.cwd),
|
||||
cwd: turn.cwd.clone(),
|
||||
#[allow(deprecated)]
|
||||
sandbox_cwd: turn.cwd.clone(),
|
||||
turn_environment: turn
|
||||
@@ -230,8 +237,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(),
|
||||
|
||||
@@ -437,7 +437,7 @@ 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, PathUri::from_path(&cwd)?);
|
||||
|
||||
wait_for_event(&test.codex, |event| {
|
||||
matches!(event, EventMsg::TurnComplete(_))
|
||||
@@ -507,8 +507,8 @@ async fn unified_exec_resolves_relative_workdir() -> Result<()> {
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
begin_event.cwd.as_path(),
|
||||
workdir.as_path(),
|
||||
begin_event.cwd,
|
||||
PathUri::from_path(&workdir)?,
|
||||
"exec_command cwd should resolve relative workdir against turn cwd",
|
||||
);
|
||||
|
||||
@@ -570,8 +570,8 @@ async fn unified_exec_respects_workdir_override() -> Result<()> {
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
begin_event.cwd.as_path(),
|
||||
workdir.as_path(),
|
||||
begin_event.cwd,
|
||||
PathUri::from_path(&workdir)?,
|
||||
"exec_command cwd should reflect the requested workdir override"
|
||||
);
|
||||
|
||||
|
||||
@@ -233,9 +233,21 @@ async fn run_codex_tool_session_inner(
|
||||
additional_permissions: _,
|
||||
available_decisions: _,
|
||||
} = ev;
|
||||
let cwd = match cwd.to_abs_path() {
|
||||
Ok(cwd) => cwd.into_path_buf(),
|
||||
Err(err) => {
|
||||
let result = create_call_tool_result_with_thread_id(
|
||||
thread_id,
|
||||
format!("Codex runtime error: {err}"),
|
||||
Some(true),
|
||||
);
|
||||
outgoing.send_response(request_id.clone(), result).await;
|
||||
break;
|
||||
}
|
||||
};
|
||||
handle_exec_approval_request(
|
||||
command,
|
||||
cwd.to_path_buf(),
|
||||
cwd,
|
||||
outgoing.clone(),
|
||||
thread.clone(),
|
||||
request_id.clone(),
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::protocol::FileChange;
|
||||
use crate::protocol::ReviewDecision;
|
||||
use crate::request_permissions::RequestPermissionProfile;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
@@ -234,7 +235,7 @@ pub struct ExecApprovalRequestEvent {
|
||||
/// The command to be executed.
|
||||
pub command: Vec<String>,
|
||||
/// The command's working directory.
|
||||
pub cwd: AbsolutePathBuf,
|
||||
pub cwd: PathUri,
|
||||
/// Optional human-readable reason for the approval (e.g. retry without sandbox).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub reason: Option<String>,
|
||||
|
||||
@@ -3242,7 +3242,7 @@ pub struct ExecCommandBeginEvent {
|
||||
/// 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,
|
||||
pub cwd: PathUri,
|
||||
pub parsed_cmd: Vec<ParsedCommand>,
|
||||
/// Where the command originated. Defaults to Agent for backward compatibility.
|
||||
#[serde(default)]
|
||||
@@ -3268,7 +3268,7 @@ pub struct ExecCommandEndEvent {
|
||||
/// 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,
|
||||
pub cwd: PathUri,
|
||||
pub parsed_cmd: Vec<ParsedCommand>,
|
||||
/// Where the command originated. Defaults to Agent for backward compatibility.
|
||||
#[serde(default)]
|
||||
|
||||
Reference in New Issue
Block a user