diff --git a/codex-rs/core/tests/common/responses.rs b/codex-rs/core/tests/common/responses.rs index 710d03fc7f..8a68b3e938 100644 --- a/codex-rs/core/tests/common/responses.rs +++ b/codex-rs/core/tests/common/responses.rs @@ -509,12 +509,17 @@ pub fn ev_apply_patch_function_call(call_id: &str, patch: &str) -> Value { } pub fn ev_shell_command_call(call_id: &str, command: &str) -> Value { - let args = serde_json::json!({ "command": command }); + let args = serde_json::json!({ "command": command, "login": false }); ev_shell_command_call_with_args(call_id, &args) } pub fn ev_shell_command_call_with_args(call_id: &str, args: &serde_json::Value) -> Value { - let arguments = serde_json::to_string(args).expect("serialize shell command arguments"); + let mut args = args.clone(); + if let serde_json::Value::Object(map) = &mut args { + map.entry("login".to_string()) + .or_insert_with(|| serde_json::Value::Bool(false)); + } + let arguments = serde_json::to_string(&args).expect("serialize shell command arguments"); ev_function_call(call_id, "shell_command", &arguments) } @@ -527,17 +532,18 @@ pub fn ev_apply_patch_shell_call(call_id: &str, patch: &str) -> Value { pub fn ev_apply_patch_shell_call_via_heredoc(call_id: &str, patch: &str) -> Value { let script = format!("apply_patch <<'EOF'\n{patch}\nEOF\n"); - let args = serde_json::json!({ "command": ["bash", "-lc", script] }); + let args = serde_json::json!({ "command": ["bash", "-c", script] }); let arguments = serde_json::to_string(&args).expect("serialize apply_patch arguments"); ev_function_call(call_id, "shell", &arguments) } pub fn ev_apply_patch_shell_command_call_via_heredoc(call_id: &str, patch: &str) -> Value { - let args = serde_json::json!({ "command": format!("apply_patch <<'EOF'\n{patch}\nEOF\n") }); - let arguments = serde_json::to_string(&args).expect("serialize apply_patch arguments"); - - ev_function_call(call_id, "shell_command", &arguments) + let args = serde_json::json!({ + "command": format!("apply_patch <<'EOF'\n{patch}\nEOF\n"), + "login": false, + }); + ev_shell_command_call_with_args(call_id, &args) } pub fn sse_failed(id: &str, code: &str, message: &str) -> String { diff --git a/codex-rs/core/tests/suite/approvals.rs b/codex-rs/core/tests/suite/approvals.rs index 51f1c31f17..cda844e9e5 100644 --- a/codex-rs/core/tests/suite/approvals.rs +++ b/codex-rs/core/tests/suite/approvals.rs @@ -30,9 +30,11 @@ use pretty_assertions::assert_eq; use regex_lite::Regex; use serde_json::Value; use serde_json::json; -use std::env; use std::fs; +use std::path::Path; use std::path::PathBuf; +use std::sync::OnceLock; +use tempfile::Builder; use wiremock::Mock; use wiremock::MockServer; use wiremock::ResponseTemplate; @@ -45,6 +47,52 @@ enum TargetPath { OutsideWorkspace(&'static str), } +fn candidate_is_outside_tmp(candidate: &Path) -> bool { + let temp_dir = std::env::temp_dir(); + if candidate.starts_with(&temp_dir) { + return false; + } + if cfg!(unix) && candidate.starts_with(Path::new("/tmp")) { + return false; + } + if let Some(tmpdir) = std::env::var_os("TMPDIR") { + let tmpdir = PathBuf::from(tmpdir); + if candidate.starts_with(&tmpdir) { + return false; + } + } + true +} + +fn outside_workspace_root() -> &'static PathBuf { + static OUTSIDE_ROOT: OnceLock = OnceLock::new(); + OUTSIDE_ROOT.get_or_init(|| { + let mut candidates = Vec::new(); + if cfg!(unix) { + candidates.push(PathBuf::from("/var/tmp")); + } + if let Some(home) = std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE")) { + candidates.push(PathBuf::from(home)); + } + for candidate in candidates { + if !candidate.is_dir() || !candidate_is_outside_tmp(&candidate) { + continue; + } + if let Ok(dir) = Builder::new() + .prefix("codex-outside-") + .tempdir_in(&candidate) + { + return dir.keep(); + } + } + Builder::new() + .prefix("codex-outside-") + .tempdir() + .expect("create outside workspace temp dir") + .keep() + }) +} + impl TargetPath { fn resolve_for_patch(self, test: &TestCodex) -> (PathBuf, String) { match self { @@ -53,9 +101,7 @@ impl TargetPath { (path, name.to_string()) } TargetPath::OutsideWorkspace(name) => { - let path = env::current_dir() - .expect("current dir should be available") - .join(name); + let path = outside_workspace_root().join(name); (path.clone(), path.display().to_string()) } } @@ -187,6 +233,7 @@ fn shell_event( ) -> Result { let mut args = json!({ "command": command, + "login": false, "timeout_ms": timeout_ms, }); if sandbox_permissions.requires_escalated_permissions() { diff --git a/codex-rs/core/tests/suite/shell_serialization.rs b/codex-rs/core/tests/suite/shell_serialization.rs index 6969b6533d..1527946f5c 100644 --- a/codex-rs/core/tests/suite/shell_serialization.rs +++ b/codex-rs/core/tests/suite/shell_serialization.rs @@ -49,6 +49,7 @@ fn shell_responses( let command = shlex::try_join(command)?; let parameters = json!({ "command": command, + "login": false, "timeout_ms": 2_000, }); Ok(vec![ diff --git a/codex-rs/core/tests/suite/unified_exec.rs b/codex-rs/core/tests/suite/unified_exec.rs index 2e2fd34e67..96e9ae3643 100644 --- a/codex-rs/core/tests/suite/unified_exec.rs +++ b/codex-rs/core/tests/suite/unified_exec.rs @@ -46,6 +46,15 @@ fn extract_output_text(item: &Value) -> Option<&str> { }) } +fn exec_command_args(mut args: Value) -> Value { + if let Value::Object(map) = &mut args + && map.contains_key("cmd") + { + map.entry("login".to_string()).or_insert(Value::Bool(false)); + } + args +} + #[derive(Debug)] struct ParsedUnifiedExecOutput { chunk_id: Option, @@ -171,10 +180,10 @@ async fn unified_exec_intercepts_apply_patch_exec_command() -> Result<()> { "*** Begin Patch\n*** Add File: uexec_apply.txt\n+hello from unified exec\n*** End Patch"; let command = format!("apply_patch <<'EOF'\n{patch}\nEOF\n"); let call_id = "uexec-apply-patch"; - let args = json!({ + let args = exec_command_args(json!({ "cmd": command, "yield_time_ms": 250, - }); + })); let responses = vec![ sse(vec![ @@ -299,11 +308,11 @@ async fn unified_exec_emits_exec_command_begin_event() -> Result<()> { } = builder.build(&server).await?; let call_id = "uexec-begin-event"; - let args = json!({ + let args = exec_command_args(json!({ "shell": "bash".to_string(), "cmd": "/bin/echo hello unified exec".to_string(), "yield_time_ms": 250, - }); + })); let responses = vec![ sse(vec![ @@ -342,7 +351,7 @@ async fn unified_exec_emits_exec_command_begin_event() -> Result<()> { }) .await; - assert_command(&begin_event.command, "-lc", "/bin/echo hello unified exec"); + assert_command(&begin_event.command, "-c", "/bin/echo hello unified exec"); assert_eq!(begin_event.cwd, cwd.path()); @@ -374,11 +383,11 @@ async fn unified_exec_resolves_relative_workdir() -> Result<()> { std::fs::create_dir_all(cwd.path().join(&workdir_rel))?; let call_id = "uexec-workdir-relative"; - let args = json!({ + let args = exec_command_args(json!({ "cmd": "pwd", "yield_time_ms": 250, "workdir": workdir_rel.to_string_lossy().to_string(), - }); + })); let responses = vec![ sse(vec![ @@ -452,11 +461,11 @@ async fn unified_exec_respects_workdir_override() -> Result<()> { std::fs::create_dir_all(&workdir)?; let call_id = "uexec-workdir"; - let args = json!({ + let args = exec_command_args(json!({ "cmd": "pwd", "yield_time_ms": 250, "workdir": workdir.to_string_lossy().to_string(), - }); + })); let responses = vec![ sse(vec![ @@ -528,16 +537,16 @@ async fn unified_exec_emits_exec_command_end_event() -> Result<()> { } = builder.build(&server).await?; let call_id = "uexec-end-event"; - let args = json!({ + let args = exec_command_args(json!({ "cmd": "/bin/echo END-EVENT".to_string(), "yield_time_ms": 250, - }); + })); let poll_call_id = "uexec-end-event-poll"; - let poll_args = json!({ + let poll_args = exec_command_args(json!({ "chars": "", "session_id": 1000, "yield_time_ms": 250, - }); + })); let responses = vec![ sse(vec![ @@ -615,10 +624,10 @@ async fn unified_exec_emits_output_delta_for_exec_command() -> Result<()> { } = builder.build(&server).await?; let call_id = "uexec-delta-1"; - let args = json!({ + let args = exec_command_args(json!({ "cmd": "printf 'HELLO-UEXEC'", "yield_time_ms": 1000, - }); + })); let responses = vec![ sse(vec![ @@ -688,10 +697,10 @@ async fn unified_exec_full_lifecycle_with_background_end_event() -> Result<()> { let call_id = "uexec-full-lifecycle"; // This timing force the long-standing PTY - let args = json!({ + let args = exec_command_args(json!({ "cmd": "sleep 0.5; printf 'HELLO-FULL-LIFECYCLE'", "yield_time_ms": 1000, - }); + })); let responses = vec![ sse(vec![ @@ -794,17 +803,17 @@ async fn unified_exec_emits_terminal_interaction_for_write_stdin() -> Result<()> } = builder.build(&server).await?; let open_call_id = "uexec-open"; - let open_args = json!({ + let open_args = exec_command_args(json!({ "cmd": "/bin/bash -i", "yield_time_ms": 200, - }); + })); let stdin_call_id = "uexec-stdin-delta"; - let stdin_args = json!({ + let stdin_args = exec_command_args(json!({ "chars": "echo WSTDIN-MARK\\n", "session_id": 1000, "yield_time_ms": 800, - }); + })); let responses = vec![ sse(vec![ @@ -893,33 +902,33 @@ async fn unified_exec_terminal_interaction_captures_delayed_output() -> Result<( } = builder.build(&server).await?; let open_call_id = "uexec-delayed-open"; - let open_args = json!({ + let open_args = exec_command_args(json!({ "cmd": "sleep 3 && echo MARKER1 && sleep 3 && echo MARKER2", "yield_time_ms": 10, - }); + })); // Poll stdin three times: first for no output, second after the first marker, // and a final long poll to capture the second marker. let first_poll_call_id = "uexec-delayed-poll-1"; - let first_poll_args = json!({ + let first_poll_args = exec_command_args(json!({ "chars": "", "session_id": 1000, "yield_time_ms": 10, - }); + })); let second_poll_call_id = "uexec-delayed-poll-2"; - let second_poll_args = json!({ + let second_poll_args = exec_command_args(json!({ "chars": "", "session_id": 1000, "yield_time_ms": 4000, - }); + })); let third_poll_call_id = "uexec-delayed-poll-3"; - let third_poll_args = json!({ + let third_poll_args = exec_command_args(json!({ "chars": "", "session_id": 1000, "yield_time_ms": 6000, - }); + })); let responses = vec![ sse(vec![ @@ -1083,18 +1092,18 @@ async fn unified_exec_emits_one_begin_and_one_end_event() -> Result<()> { } = builder.build(&server).await?; let open_call_id = "uexec-open-session"; - let open_args = json!({ + let open_args = exec_command_args(json!({ "shell": "bash".to_string(), "cmd": "sleep 0.1".to_string(), "yield_time_ms": 10, - }); + })); let poll_call_id = "uexec-poll-empty"; - let poll_args = json!({ + let poll_args = exec_command_args(json!({ "chars": "", "session_id": 1000, "yield_time_ms": 150, - }); + })); let responses = vec![ sse(vec![ @@ -1166,7 +1175,7 @@ async fn unified_exec_emits_one_begin_and_one_end_event() -> Result<()> { let open_event = &begin_events[0]; - assert_command(&open_event.command, "-lc", "sleep 0.1"); + assert_command(&open_event.command, "-c", "sleep 0.1"); assert!( open_event.interaction_input.is_none(), @@ -1199,11 +1208,11 @@ async fn exec_command_reports_chunk_and_exit_metadata() -> Result<()> { } = builder.build(&server).await?; let call_id = "uexec-metadata"; - let args = serde_json::json!({ + let args = exec_command_args(serde_json::json!({ "cmd": "printf 'token one token two token three token four token five token six token seven'", "yield_time_ms": 500, "max_output_tokens": 6, - }); + })); let responses = vec![ sse(vec![ @@ -1306,10 +1315,10 @@ async fn unified_exec_respects_early_exit_notifications() -> Result<()> { } = builder.build(&server).await?; let call_id = "uexec-early-exit"; - let args = serde_json::json!({ + let args = exec_command_args(serde_json::json!({ "cmd": "sleep 0.05", "yield_time_ms": 31415, - }); + })); let responses = vec![ sse(vec![ @@ -1401,20 +1410,20 @@ async fn write_stdin_returns_exit_metadata_and_clears_session() -> Result<()> { let send_call_id = "uexec-cat-send"; let exit_call_id = "uexec-cat-exit"; - let start_args = serde_json::json!({ + let start_args = exec_command_args(serde_json::json!({ "cmd": "/bin/cat", "yield_time_ms": 500, - }); - let send_args = serde_json::json!({ + })); + let send_args = exec_command_args(serde_json::json!({ "chars": "hello unified exec\n", "session_id": 1000, "yield_time_ms": 500, - }); - let exit_args = serde_json::json!({ + })); + let exit_args = exec_command_args(serde_json::json!({ "chars": "\u{0004}", "session_id": 1000, "yield_time_ms": 500, - }); + })); let responses = vec![ sse(vec![ @@ -1560,24 +1569,24 @@ async fn unified_exec_emits_end_event_when_session_dies_via_stdin() -> Result<() } = builder.build(&server).await?; let start_call_id = "uexec-end-on-exit-start"; - let start_args = serde_json::json!({ + let start_args = exec_command_args(serde_json::json!({ "cmd": "/bin/cat", "yield_time_ms": 200, - }); + })); let echo_call_id = "uexec-end-on-exit-echo"; - let echo_args = serde_json::json!({ + let echo_args = exec_command_args(serde_json::json!({ "chars": "bye-END\n", "session_id": 1000, "yield_time_ms": 300, - }); + })); let exit_call_id = "uexec-end-on-exit"; - let exit_args = serde_json::json!({ + let exit_args = exec_command_args(serde_json::json!({ "chars": "\u{0004}", "session_id": 1000, "yield_time_ms": 500, - }); + })); let responses = vec![ sse(vec![ @@ -1670,10 +1679,10 @@ async fn unified_exec_closes_long_running_session_at_turn_end() -> Result<()> { let call_id = "uexec-long-running"; let command = format!("printf '%s' $$ > '{pid_path_str}' && exec sleep 3000"); - let args = json!({ + let args = exec_command_args(json!({ "cmd": command, "yield_time_ms": 250, - }); + })); let responses = vec![ sse(vec![ @@ -1769,17 +1778,17 @@ async fn unified_exec_reuses_session_via_stdin() -> Result<()> { } = builder.build(&server).await?; let first_call_id = "uexec-start"; - let first_args = serde_json::json!({ + let first_args = exec_command_args(serde_json::json!({ "cmd": "/bin/cat", "yield_time_ms": 200, - }); + })); let second_call_id = "uexec-stdin"; - let second_args = serde_json::json!({ + let second_args = exec_command_args(serde_json::json!({ "chars": "hello unified exec\n", "session_id": 1000, "yield_time_ms": 500, - }); + })); let responses = vec![ sse(vec![ @@ -1900,17 +1909,17 @@ PY "#; let first_call_id = "uexec-lag-start"; - let first_args = serde_json::json!({ + let first_args = exec_command_args(serde_json::json!({ "cmd": script, "yield_time_ms": 25, - }); + })); let second_call_id = "uexec-lag-poll"; - let second_args = serde_json::json!({ + let second_args = exec_command_args(serde_json::json!({ "chars": "", "session_id": 1000, "yield_time_ms": 2_000, - }); + })); let responses = vec![ sse(vec![ @@ -2011,17 +2020,17 @@ async fn unified_exec_timeout_and_followup_poll() -> Result<()> { } = builder.build(&server).await?; let first_call_id = "uexec-timeout"; - let first_args = serde_json::json!({ + let first_args = exec_command_args(serde_json::json!({ "cmd": "sleep 0.5; echo ready", "yield_time_ms": 10, - }); + })); let second_call_id = "uexec-poll"; - let second_args = serde_json::json!({ + let second_args = exec_command_args(serde_json::json!({ "chars": "", "session_id": 1000, "yield_time_ms": 800, - }); + })); let responses = vec![ sse(vec![ @@ -2123,11 +2132,11 @@ PY "#; let call_id = "uexec-large-output"; - let args = serde_json::json!({ + let args = exec_command_args(serde_json::json!({ "cmd": script, "max_output_tokens": 100, "yield_time_ms": 500, - }); + })); let responses = vec![ sse(vec![ @@ -2202,10 +2211,10 @@ async fn unified_exec_runs_under_sandbox() -> Result<()> { } = builder.build(&server).await?; let call_id = "uexec"; - let args = serde_json::json!({ + let args = exec_command_args(serde_json::json!({ "cmd": "echo 'hello'", "yield_time_ms": 500, - }); + })); let responses = vec![ sse(vec![ @@ -2282,17 +2291,17 @@ async fn unified_exec_python_prompt_under_seatbelt() -> Result<()> { } = builder.build(&server).await?; let startup_call_id = "uexec-python-seatbelt"; - let startup_args = serde_json::json!({ + let startup_args = exec_command_args(serde_json::json!({ "cmd": format!("{} -i", python.display()), "yield_time_ms": 1_500, - }); + })); let exit_call_id = "uexec-python-exit"; - let exit_args = serde_json::json!({ + let exit_args = exec_command_args(serde_json::json!({ "chars": "exit()\n", "session_id": 1000, "yield_time_ms": 1_500, - }); + })); let responses = vec![ sse(vec![ @@ -2396,9 +2405,9 @@ async fn unified_exec_runs_on_all_platforms() -> Result<()> { } = builder.build(&server).await?; let call_id = "uexec"; - let args = serde_json::json!({ + let args = exec_command_args(serde_json::json!({ "cmd": "echo 'hello crossplat'", - }); + })); let responses = vec![ sse(vec![ @@ -2472,17 +2481,17 @@ async fn unified_exec_prunes_exited_sessions_first() -> Result<()> { const FILLER_SESSIONS: i32 = MAX_SESSIONS_FOR_TEST - 1; let keep_call_id = "uexec-prune-keep"; - let keep_args = serde_json::json!({ + let keep_args = exec_command_args(serde_json::json!({ "cmd": "/bin/cat", "yield_time_ms": 250, - }); + })); let prune_call_id = "uexec-prune-target"; // Give the sleeper time to exit before the filler sessions trigger pruning. - let prune_args = serde_json::json!({ + let prune_args = exec_command_args(serde_json::json!({ "cmd": "sleep 1", "yield_time_ms": 1_250, - }); + })); let mut events = vec![ev_response_created("resp-prune-1")]; events.push(ev_function_call( @@ -2497,10 +2506,10 @@ async fn unified_exec_prunes_exited_sessions_first() -> Result<()> { )); for idx in 0..FILLER_SESSIONS { - let filler_args = serde_json::json!({ + let filler_args = exec_command_args(serde_json::json!({ "cmd": format!("echo filler {idx}"), "yield_time_ms": 250, - }); + })); let call_id = format!("uexec-prune-fill-{idx}"); events.push(ev_function_call( &call_id, @@ -2510,11 +2519,11 @@ async fn unified_exec_prunes_exited_sessions_first() -> Result<()> { } let keep_write_call_id = "uexec-prune-keep-write"; - let keep_write_args = serde_json::json!({ + let keep_write_args = exec_command_args(serde_json::json!({ "chars": "still alive\n", "session_id": 1000, "yield_time_ms": 500, - }); + })); events.push(ev_function_call( keep_write_call_id, "write_stdin", @@ -2522,11 +2531,11 @@ async fn unified_exec_prunes_exited_sessions_first() -> Result<()> { )); let probe_call_id = "uexec-prune-probe"; - let probe_args = serde_json::json!({ + let probe_args = exec_command_args(serde_json::json!({ "chars": "should fail\n", "session_id": 1001, "yield_time_ms": 500, - }); + })); events.push(ev_function_call( probe_call_id, "write_stdin",