mirror of
https://github.com/openai/codex.git
synced 2026-02-07 01:13:40 +00:00
Compare commits
2 Commits
remove/doc
...
dh--unifie
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
119b7780bc | ||
|
|
0b09838b32 |
@@ -30,6 +30,7 @@ use crate::spawn::StdioPolicy;
|
||||
use crate::spawn::spawn_child_async;
|
||||
|
||||
const DEFAULT_TIMEOUT_MS: u64 = 10_000;
|
||||
const MAX_EXEC_TIMEOUT_MS: u64 = 120_000;
|
||||
|
||||
// Hardcode these since it does not seem worth including the libc crate just
|
||||
// for these.
|
||||
@@ -59,7 +60,9 @@ pub struct ExecParams {
|
||||
|
||||
impl ExecParams {
|
||||
pub fn timeout_duration(&self) -> Duration {
|
||||
Duration::from_millis(self.timeout_ms.unwrap_or(DEFAULT_TIMEOUT_MS))
|
||||
let raw_timeout_ms = self.timeout_ms.unwrap_or(DEFAULT_TIMEOUT_MS);
|
||||
let clamped_timeout_ms = raw_timeout_ms.min(MAX_EXEC_TIMEOUT_MS);
|
||||
Duration::from_millis(clamped_timeout_ms)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,6 +185,8 @@ async fn exec_windows_sandbox(
|
||||
..
|
||||
} = params;
|
||||
|
||||
let timeout_ms = timeout_ms.map(|value| value.min(MAX_EXEC_TIMEOUT_MS));
|
||||
|
||||
let policy_str = match sandbox_policy {
|
||||
SandboxPolicy::DangerFullAccess => "workspace-write",
|
||||
SandboxPolicy::ReadOnly => "read-only",
|
||||
|
||||
@@ -25,6 +25,23 @@ where
|
||||
ShellEnvironmentPolicyInherit::All => vars.into_iter().collect(),
|
||||
ShellEnvironmentPolicyInherit::None => HashMap::new(),
|
||||
ShellEnvironmentPolicyInherit::Core => {
|
||||
#[cfg(target_os = "windows")]
|
||||
const CORE_VARS: &[&str] = &[
|
||||
"COMSPEC",
|
||||
"HOME",
|
||||
"LOGNAME",
|
||||
"PATH",
|
||||
"PATHEXT",
|
||||
"SYSTEMROOT",
|
||||
"TEMP",
|
||||
"TMP",
|
||||
"TMPDIR",
|
||||
"USER",
|
||||
"USERNAME",
|
||||
"USERPROFILE",
|
||||
"WINDIR",
|
||||
];
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
const CORE_VARS: &[&str] = &[
|
||||
"HOME", "LOGNAME", "PATH", "SHELL", "USER", "USERNAME", "TMPDIR", "TEMP", "TMP",
|
||||
];
|
||||
|
||||
@@ -54,7 +54,14 @@ struct WriteStdinArgs {
|
||||
}
|
||||
|
||||
fn default_shell() -> String {
|
||||
"/bin/bash".to_string()
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
"powershell.exe".to_string()
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
"/bin/bash".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn default_login() -> bool {
|
||||
|
||||
@@ -277,7 +277,10 @@ fn create_shell_tool() -> ToolSpec {
|
||||
properties.insert(
|
||||
"timeout_ms".to_string(),
|
||||
JsonSchema::Number {
|
||||
description: Some("The timeout for the command in milliseconds".to_string()),
|
||||
description: Some(
|
||||
"The timeout for the command in milliseconds (clamped to a maximum of 120000 ms / 2 minutes)."
|
||||
.to_string(),
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -325,7 +328,10 @@ fn create_shell_command_tool() -> ToolSpec {
|
||||
properties.insert(
|
||||
"timeout_ms".to_string(),
|
||||
JsonSchema::Number {
|
||||
description: Some("The timeout for the command in milliseconds".to_string()),
|
||||
description: Some(
|
||||
"The timeout for the command in milliseconds (clamped to a maximum of 120000 ms / 2 minutes)."
|
||||
.to_string(),
|
||||
),
|
||||
},
|
||||
);
|
||||
properties.insert(
|
||||
|
||||
@@ -43,7 +43,7 @@ pub(crate) use session::UnifiedExecSession;
|
||||
|
||||
pub(crate) const DEFAULT_YIELD_TIME_MS: u64 = 10_000;
|
||||
pub(crate) const MIN_YIELD_TIME_MS: u64 = 250;
|
||||
pub(crate) const MAX_YIELD_TIME_MS: u64 = 30_000;
|
||||
pub(crate) const MAX_YIELD_TIME_MS: u64 = 120_000;
|
||||
pub(crate) const DEFAULT_MAX_OUTPUT_TOKENS: usize = 10_000;
|
||||
pub(crate) const UNIFIED_EXEC_OUTPUT_MAX_BYTES: usize = 1024 * 1024; // 1 MiB
|
||||
|
||||
|
||||
@@ -130,51 +130,58 @@ impl UnifiedExecSession {
|
||||
self.session.exit_code()
|
||||
}
|
||||
|
||||
async fn snapshot_output(&self) -> Vec<Vec<u8>> {
|
||||
let guard = self.output_buffer.lock().await;
|
||||
guard.snapshot()
|
||||
}
|
||||
|
||||
fn sandbox_type(&self) -> SandboxType {
|
||||
self.sandbox_type
|
||||
}
|
||||
|
||||
pub(super) async fn check_for_sandbox_denial(&self) -> Result<(), UnifiedExecError> {
|
||||
if self.sandbox_type() == SandboxType::None || !self.has_exited() {
|
||||
return Ok(());
|
||||
}
|
||||
pub(super) fn check_for_sandbox_denial(
|
||||
&self,
|
||||
) -> impl std::future::Future<Output = Result<(), UnifiedExecError>> + Send {
|
||||
let sandbox_type = self.sandbox_type();
|
||||
let has_exited = self.has_exited();
|
||||
let output_notify = Arc::clone(&self.output_notify);
|
||||
let output_buffer = Arc::clone(&self.output_buffer);
|
||||
let exit_code = self.exit_code();
|
||||
|
||||
let _ =
|
||||
tokio::time::timeout(Duration::from_millis(20), self.output_notify.notified()).await;
|
||||
async move {
|
||||
if sandbox_type == SandboxType::None || !has_exited {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let collected_chunks = self.snapshot_output().await;
|
||||
let mut aggregated: Vec<u8> = Vec::new();
|
||||
for chunk in collected_chunks {
|
||||
aggregated.extend_from_slice(&chunk);
|
||||
}
|
||||
let aggregated_text = String::from_utf8_lossy(&aggregated).to_string();
|
||||
let exit_code = self.exit_code().unwrap_or(-1);
|
||||
let _ = tokio::time::timeout(Duration::from_millis(20), output_notify.notified()).await;
|
||||
|
||||
let exec_output = ExecToolCallOutput {
|
||||
exit_code,
|
||||
stdout: StreamOutput::new(aggregated_text.clone()),
|
||||
stderr: StreamOutput::new(String::new()),
|
||||
aggregated_output: StreamOutput::new(aggregated_text.clone()),
|
||||
duration: Duration::ZERO,
|
||||
timed_out: false,
|
||||
};
|
||||
|
||||
if is_likely_sandbox_denied(self.sandbox_type(), &exec_output) {
|
||||
let (snippet, _) = truncate_middle(&aggregated_text, UNIFIED_EXEC_OUTPUT_MAX_BYTES);
|
||||
let message = if snippet.is_empty() {
|
||||
format!("exit code {exit_code}")
|
||||
} else {
|
||||
snippet
|
||||
let collected_chunks = {
|
||||
let guard = output_buffer.lock().await;
|
||||
guard.snapshot()
|
||||
};
|
||||
return Err(UnifiedExecError::sandbox_denied(message, exec_output));
|
||||
}
|
||||
let mut aggregated: Vec<u8> = Vec::new();
|
||||
for chunk in collected_chunks {
|
||||
aggregated.extend_from_slice(&chunk);
|
||||
}
|
||||
let aggregated_text = String::from_utf8_lossy(&aggregated).to_string();
|
||||
let exit_code = exit_code.unwrap_or(-1);
|
||||
|
||||
Ok(())
|
||||
let exec_output = ExecToolCallOutput {
|
||||
exit_code,
|
||||
stdout: StreamOutput::new(aggregated_text.clone()),
|
||||
stderr: StreamOutput::new(String::new()),
|
||||
aggregated_output: StreamOutput::new(aggregated_text.clone()),
|
||||
duration: Duration::ZERO,
|
||||
timed_out: false,
|
||||
};
|
||||
|
||||
if is_likely_sandbox_denied(sandbox_type, &exec_output) {
|
||||
let (snippet, _) = truncate_middle(&aggregated_text, UNIFIED_EXEC_OUTPUT_MAX_BYTES);
|
||||
let message = if snippet.is_empty() {
|
||||
format!("exit code {exit_code}")
|
||||
} else {
|
||||
snippet
|
||||
};
|
||||
return Err(UnifiedExecError::sandbox_denied(message, exec_output));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn from_spawned(
|
||||
|
||||
@@ -43,12 +43,7 @@ impl UnifiedExecSessionManager {
|
||||
.workdir
|
||||
.clone()
|
||||
.unwrap_or_else(|| context.turn.cwd.clone());
|
||||
let shell_flag = if request.login { "-lc" } else { "-c" };
|
||||
let command = vec![
|
||||
request.shell.to_string(),
|
||||
shell_flag.to_string(),
|
||||
request.command.to_string(),
|
||||
];
|
||||
let command = Self::build_shell_command(request.shell, request.login, request.command);
|
||||
|
||||
let session = self
|
||||
.open_session_with_sandbox(
|
||||
@@ -111,6 +106,30 @@ impl UnifiedExecSessionManager {
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn build_shell_command(shell: &str, login: bool, command: &str) -> Vec<String> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let _ = login;
|
||||
let shell_lower = shell.to_ascii_lowercase();
|
||||
let flag = if shell_lower.contains("cmd") {
|
||||
"/C"
|
||||
} else {
|
||||
"-Command"
|
||||
};
|
||||
vec![shell.to_string(), flag.to_string(), command.to_string()]
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let shell_flag = if login { "-lc" } else { "-c" };
|
||||
vec![
|
||||
shell.to_string(),
|
||||
shell_flag.to_string(),
|
||||
command.to_string(),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn write_stdin(
|
||||
&self,
|
||||
request: WriteStdinRequest<'_>,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#![cfg(not(target_os = "windows"))]
|
||||
use std::collections::HashMap;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
@@ -151,6 +150,104 @@ fn collect_tool_outputs(bodies: &[Value]) -> Result<HashMap<String, ParsedUnifie
|
||||
Ok(outputs)
|
||||
}
|
||||
|
||||
fn echo_command(text: &str) -> String {
|
||||
if cfg!(target_os = "windows") {
|
||||
format!("Write-Output \"{text}\"")
|
||||
} else {
|
||||
format!("/bin/echo {text}")
|
||||
}
|
||||
}
|
||||
|
||||
fn print_no_newline_command(text: &str) -> String {
|
||||
if cfg!(target_os = "windows") {
|
||||
format!("[Console]::Write('{text}')")
|
||||
} else {
|
||||
format!("printf '{text}'")
|
||||
}
|
||||
}
|
||||
|
||||
fn current_dir_command() -> String {
|
||||
if cfg!(target_os = "windows") {
|
||||
"[Environment]::CurrentDirectory".to_string()
|
||||
} else {
|
||||
"pwd".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn ready_command() -> String {
|
||||
echo_command("ready")
|
||||
}
|
||||
|
||||
fn cat_like_command() -> String {
|
||||
if cfg!(target_os = "windows") {
|
||||
"while (($line = [Console]::In.ReadLine()) -ne $null) { if ($line -eq '__EXIT__') { break }; Write-Output $line }".to_string()
|
||||
} else {
|
||||
"/bin/cat".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn cat_exit_input() -> &'static str {
|
||||
if cfg!(target_os = "windows") {
|
||||
"__EXIT__\n"
|
||||
} else {
|
||||
"\u{0004}"
|
||||
}
|
||||
}
|
||||
|
||||
fn sleep_then_ready_command() -> String {
|
||||
if cfg!(target_os = "windows") {
|
||||
"Start-Sleep -Seconds 0.5; Write-Output 'ready'".to_string()
|
||||
} else {
|
||||
"sleep 0.5; echo ready".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn laggy_output_script() -> String {
|
||||
if cfg!(target_os = "windows") {
|
||||
concat!(
|
||||
"$chunk = 'x' * 1048576; ",
|
||||
"1..4 | ForEach-Object { [Console]::Write($chunk); [Console]::Out.Flush() }; ",
|
||||
"Start-Sleep -Milliseconds 200; ",
|
||||
"1..5 | ForEach-Object { Write-Output 'TAIL-MARKER'; Start-Sleep -Milliseconds 50 }; ",
|
||||
"Start-Sleep -Milliseconds 200",
|
||||
)
|
||||
.to_string()
|
||||
} else {
|
||||
r#"python3 - <<'PY'
|
||||
import sys
|
||||
import time
|
||||
|
||||
chunk = b'x' * (1 << 20)
|
||||
for _ in range(4):
|
||||
sys.stdout.buffer.write(chunk)
|
||||
sys.stdout.flush()
|
||||
|
||||
time.sleep(0.2)
|
||||
for _ in range(5):
|
||||
sys.stdout.write("TAIL-MARKER\n")
|
||||
sys.stdout.flush()
|
||||
time.sleep(0.05)
|
||||
|
||||
time.sleep(0.2)
|
||||
PY
|
||||
"#
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn large_output_script() -> String {
|
||||
if cfg!(target_os = "windows") {
|
||||
"1..300 | ForEach-Object { \"line-$_\" }".to_string()
|
||||
} else {
|
||||
r#"python3 - <<'PY'
|
||||
for i in range(300):
|
||||
print(f"line-{i}")
|
||||
PY
|
||||
"#
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn unified_exec_emits_exec_command_begin_event() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
@@ -171,7 +268,7 @@ async fn unified_exec_emits_exec_command_begin_event() -> Result<()> {
|
||||
|
||||
let call_id = "uexec-begin-event";
|
||||
let args = json!({
|
||||
"cmd": "/bin/echo hello unified exec".to_string(),
|
||||
"cmd": echo_command("hello unified exec"),
|
||||
"yield_time_ms": 250,
|
||||
});
|
||||
|
||||
@@ -212,10 +309,8 @@ async fn unified_exec_emits_exec_command_begin_event() -> Result<()> {
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
begin_event.command,
|
||||
vec!["/bin/echo hello unified exec".to_string()]
|
||||
);
|
||||
let expected_command = vec![echo_command("hello unified exec")];
|
||||
assert_eq!(begin_event.command, expected_command);
|
||||
assert_eq!(begin_event.cwd, cwd.path());
|
||||
|
||||
wait_for_event(&codex, |event| matches!(event, EventMsg::TaskComplete(_))).await;
|
||||
@@ -245,11 +340,17 @@ async fn unified_exec_respects_workdir_override() -> Result<()> {
|
||||
std::fs::create_dir_all(&workdir)?;
|
||||
|
||||
let call_id = "uexec-workdir";
|
||||
let args = json!({
|
||||
"cmd": "pwd",
|
||||
let mut args = json!({
|
||||
"cmd": current_dir_command(),
|
||||
"yield_time_ms": 250,
|
||||
"workdir": workdir.to_string_lossy().to_string(),
|
||||
});
|
||||
if cfg!(target_os = "windows")
|
||||
&& let Some(obj) = args.as_object_mut()
|
||||
{
|
||||
obj.insert("shell".to_string(), json!("cmd.exe"));
|
||||
obj.insert("login".to_string(), json!(false));
|
||||
}
|
||||
|
||||
let responses = vec![
|
||||
sse(vec![
|
||||
@@ -297,7 +398,19 @@ async fn unified_exec_respects_workdir_override() -> Result<()> {
|
||||
.get(call_id)
|
||||
.expect("missing exec_command workdir output");
|
||||
let output_text = output.output.trim();
|
||||
let output_canonical = std::fs::canonicalize(output_text)?;
|
||||
assert!(
|
||||
!output_text.is_empty(),
|
||||
"workdir command should produce a path (raw output: {raw:?}, exit_code: {exit_code:?}, session_id: {session_id:?})",
|
||||
raw = output.output,
|
||||
exit_code = output.exit_code,
|
||||
session_id = output.session_id
|
||||
);
|
||||
let output_path = std::path::PathBuf::from(output_text);
|
||||
assert!(
|
||||
output_path.exists(),
|
||||
"workdir output path does not exist: {output_text}"
|
||||
);
|
||||
let output_canonical = std::fs::canonicalize(output_path)?;
|
||||
let expected_canonical = std::fs::canonicalize(&workdir)?;
|
||||
assert_eq!(
|
||||
output_canonical, expected_canonical,
|
||||
@@ -327,7 +440,7 @@ async fn unified_exec_emits_exec_command_end_event() -> Result<()> {
|
||||
|
||||
let call_id = "uexec-end-event";
|
||||
let args = json!({
|
||||
"cmd": "/bin/echo END-EVENT".to_string(),
|
||||
"cmd": echo_command("END-EVENT"),
|
||||
"yield_time_ms": 250,
|
||||
});
|
||||
let poll_call_id = "uexec-end-event-poll";
|
||||
@@ -413,7 +526,7 @@ async fn unified_exec_emits_output_delta_for_exec_command() -> Result<()> {
|
||||
|
||||
let call_id = "uexec-delta-1";
|
||||
let args = json!({
|
||||
"cmd": "printf 'HELLO-UEXEC'",
|
||||
"cmd": print_no_newline_command("HELLO-UEXEC"),
|
||||
"yield_time_ms": 1000,
|
||||
});
|
||||
|
||||
@@ -484,7 +597,7 @@ async fn unified_exec_emits_output_delta_for_write_stdin() -> Result<()> {
|
||||
|
||||
let open_call_id = "uexec-open";
|
||||
let open_args = json!({
|
||||
"cmd": "/bin/bash -i",
|
||||
"cmd": cat_like_command(),
|
||||
"yield_time_ms": 200,
|
||||
});
|
||||
|
||||
@@ -582,8 +695,9 @@ async fn unified_exec_skips_begin_event_for_empty_input() -> Result<()> {
|
||||
} = builder.build(&server).await?;
|
||||
|
||||
let open_call_id = "uexec-open-session";
|
||||
let open_cmd = ready_command();
|
||||
let open_args = json!({
|
||||
"cmd": "/bin/sh -c echo ready".to_string(),
|
||||
"cmd": open_cmd,
|
||||
"yield_time_ms": 250,
|
||||
});
|
||||
|
||||
@@ -654,7 +768,7 @@ async fn unified_exec_skips_begin_event_for_empty_input() -> Result<()> {
|
||||
"expected only the initial command to emit begin event"
|
||||
);
|
||||
assert_eq!(begin_events[0].call_id, open_call_id);
|
||||
assert_eq!(begin_events[0].command[0], "/bin/sh -c echo ready");
|
||||
assert_eq!(begin_events[0].command[0], open_cmd);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -678,7 +792,7 @@ async fn exec_command_reports_chunk_and_exit_metadata() -> Result<()> {
|
||||
|
||||
let call_id = "uexec-metadata";
|
||||
let args = serde_json::json!({
|
||||
"cmd": "printf 'abcdefghijklmnopqrstuvwxyz'",
|
||||
"cmd": print_no_newline_command("abcdefghijklmnopqrstuvwxyz"),
|
||||
"yield_time_ms": 500,
|
||||
"max_output_tokens": 6,
|
||||
});
|
||||
@@ -788,7 +902,7 @@ async fn write_stdin_returns_exit_metadata_and_clears_session() -> Result<()> {
|
||||
let exit_call_id = "uexec-cat-exit";
|
||||
|
||||
let start_args = serde_json::json!({
|
||||
"cmd": "/bin/cat",
|
||||
"cmd": cat_like_command(),
|
||||
"yield_time_ms": 500,
|
||||
});
|
||||
let send_args = serde_json::json!({
|
||||
@@ -797,7 +911,7 @@ async fn write_stdin_returns_exit_metadata_and_clears_session() -> Result<()> {
|
||||
"yield_time_ms": 500,
|
||||
});
|
||||
let exit_args = serde_json::json!({
|
||||
"chars": "\u{0004}",
|
||||
"chars": cat_exit_input(),
|
||||
"session_id": 0,
|
||||
"yield_time_ms": 500,
|
||||
});
|
||||
@@ -945,7 +1059,7 @@ async fn unified_exec_emits_end_event_when_session_dies_via_stdin() -> Result<()
|
||||
|
||||
let start_call_id = "uexec-end-on-exit-start";
|
||||
let start_args = serde_json::json!({
|
||||
"cmd": "/bin/cat",
|
||||
"cmd": cat_like_command(),
|
||||
"yield_time_ms": 200,
|
||||
});
|
||||
|
||||
@@ -958,7 +1072,7 @@ async fn unified_exec_emits_end_event_when_session_dies_via_stdin() -> Result<()
|
||||
|
||||
let exit_call_id = "uexec-end-on-exit";
|
||||
let exit_args = serde_json::json!({
|
||||
"chars": "\u{0004}",
|
||||
"chars": cat_exit_input(),
|
||||
"session_id": 0,
|
||||
"yield_time_ms": 500,
|
||||
});
|
||||
@@ -1048,7 +1162,7 @@ async fn unified_exec_reuses_session_via_stdin() -> Result<()> {
|
||||
|
||||
let first_call_id = "uexec-start";
|
||||
let first_args = serde_json::json!({
|
||||
"cmd": "/bin/cat",
|
||||
"cmd": cat_like_command(),
|
||||
"yield_time_ms": 200,
|
||||
});
|
||||
|
||||
@@ -1155,24 +1269,7 @@ async fn unified_exec_streams_after_lagged_output() -> Result<()> {
|
||||
..
|
||||
} = builder.build(&server).await?;
|
||||
|
||||
let script = r#"python3 - <<'PY'
|
||||
import sys
|
||||
import time
|
||||
|
||||
chunk = b'x' * (1 << 20)
|
||||
for _ in range(4):
|
||||
sys.stdout.buffer.write(chunk)
|
||||
sys.stdout.flush()
|
||||
|
||||
time.sleep(0.2)
|
||||
for _ in range(5):
|
||||
sys.stdout.write("TAIL-MARKER\n")
|
||||
sys.stdout.flush()
|
||||
time.sleep(0.05)
|
||||
|
||||
time.sleep(0.2)
|
||||
PY
|
||||
"#;
|
||||
let script = laggy_output_script();
|
||||
|
||||
let first_call_id = "uexec-lag-start";
|
||||
let first_args = serde_json::json!({
|
||||
@@ -1282,7 +1379,7 @@ async fn unified_exec_timeout_and_followup_poll() -> Result<()> {
|
||||
|
||||
let first_call_id = "uexec-timeout";
|
||||
let first_args = serde_json::json!({
|
||||
"cmd": "sleep 0.5; echo ready",
|
||||
"cmd": sleep_then_ready_command(),
|
||||
"yield_time_ms": 10,
|
||||
});
|
||||
|
||||
@@ -1386,11 +1483,7 @@ async fn unified_exec_formats_large_output_summary() -> Result<()> {
|
||||
..
|
||||
} = builder.build(&server).await?;
|
||||
|
||||
let script = r#"python3 - <<'PY'
|
||||
for i in range(300):
|
||||
print(f"line-{i}")
|
||||
PY
|
||||
"#;
|
||||
let script = large_output_script();
|
||||
|
||||
let call_id = "uexec-large-output";
|
||||
let args = serde_json::json!({
|
||||
@@ -1473,7 +1566,7 @@ async fn unified_exec_runs_under_sandbox() -> Result<()> {
|
||||
|
||||
let call_id = "uexec";
|
||||
let args = serde_json::json!({
|
||||
"cmd": "echo 'hello'",
|
||||
"cmd": echo_command("hello"),
|
||||
"yield_time_ms": 500,
|
||||
});
|
||||
|
||||
|
||||
12
codex-rs/patch.txt
Normal file
12
codex-rs/patch.txt
Normal file
@@ -0,0 +1,12 @@
|
||||
*** Begin Patch
|
||||
*** Update File: core\tests\suite\unified_exec.rs
|
||||
@@
|
||||
fn current_dir_command() -> String {
|
||||
if cfg!(target_os = "windows") {
|
||||
- "[Environment]::CurrentDirectory".to_string()
|
||||
+ "cd".to_string()
|
||||
} else {
|
||||
"pwd".to_string()
|
||||
}
|
||||
}
|
||||
*** End Patch
|
||||
@@ -16,8 +16,8 @@ use tokio::sync::oneshot;
|
||||
use tokio::sync::Mutex as TokioMutex;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ExecCommandSession {
|
||||
master: Box<dyn portable_pty::MasterPty + Send>,
|
||||
writer_tx: mpsc::Sender<Vec<u8>>,
|
||||
output_tx: broadcast::Sender<Vec<u8>>,
|
||||
killer: StdMutex<Option<Box<dyn portable_pty::ChildKiller + Send + Sync>>>,
|
||||
@@ -28,9 +28,19 @@ pub struct ExecCommandSession {
|
||||
exit_code: Arc<StdMutex<Option<i32>>>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ExecCommandSession {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ExecCommandSession")
|
||||
.field("exit_status", &self.exit_status)
|
||||
.field("exit_code", &self.exit_code)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl ExecCommandSession {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
master: Box<dyn portable_pty::MasterPty + Send>,
|
||||
writer_tx: mpsc::Sender<Vec<u8>>,
|
||||
output_tx: broadcast::Sender<Vec<u8>>,
|
||||
killer: Box<dyn portable_pty::ChildKiller + Send + Sync>,
|
||||
@@ -43,6 +53,7 @@ impl ExecCommandSession {
|
||||
let initial_output_rx = output_tx.subscribe();
|
||||
(
|
||||
Self {
|
||||
master,
|
||||
writer_tx,
|
||||
output_tx,
|
||||
killer: StdMutex::new(Some(killer)),
|
||||
@@ -125,9 +136,22 @@ pub async fn spawn_pty_process(
|
||||
pixel_height: 0,
|
||||
})?;
|
||||
|
||||
let mut command_builder = CommandBuilder::new(arg0.as_ref().unwrap_or(&program.to_string()));
|
||||
let master = pair.master;
|
||||
let mut slave = pair.slave;
|
||||
|
||||
let mut command_builder = CommandBuilder::new(program);
|
||||
let _ = arg0;
|
||||
command_builder.cwd(cwd);
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
command_builder.env_clear();
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// Keep the inherited Windows environment to avoid missing critical
|
||||
// variables that cause console hosts to fail to initialize.
|
||||
for (key, value) in std::env::vars() {
|
||||
command_builder.env(key, value);
|
||||
}
|
||||
}
|
||||
for arg in args {
|
||||
command_builder.arg(arg);
|
||||
}
|
||||
@@ -135,13 +159,33 @@ pub async fn spawn_pty_process(
|
||||
command_builder.env(key, value);
|
||||
}
|
||||
|
||||
let mut child = pair.slave.spawn_command(command_builder)?;
|
||||
#[cfg(all(test, target_os = "windows"))]
|
||||
eprintln!(
|
||||
"spawn_pty_process env keys: {:?}",
|
||||
env.keys().cloned().collect::<Vec<_>>()
|
||||
);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// Ensure core OS variables are present even if the provided env map
|
||||
// was minimized.
|
||||
for key in ["SystemRoot", "WINDIR", "COMSPEC", "PATHEXT", "PATH"] {
|
||||
if !env.contains_key(key) {
|
||||
if let Ok(value) = std::env::var(key) {
|
||||
command_builder.env(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut child = slave.spawn_command(command_builder)?;
|
||||
drop(slave);
|
||||
let killer = child.clone_killer();
|
||||
|
||||
let (writer_tx, mut writer_rx) = mpsc::channel::<Vec<u8>>(128);
|
||||
let (output_tx, _) = broadcast::channel::<Vec<u8>>(256);
|
||||
|
||||
let mut reader = pair.master.try_clone_reader()?;
|
||||
let mut reader = master.try_clone_reader()?;
|
||||
let output_tx_clone = output_tx.clone();
|
||||
let reader_handle: JoinHandle<()> = tokio::task::spawn_blocking(move || {
|
||||
let mut buf = [0u8; 8_192];
|
||||
@@ -161,7 +205,7 @@ pub async fn spawn_pty_process(
|
||||
}
|
||||
});
|
||||
|
||||
let writer = pair.master.take_writer()?;
|
||||
let writer = master.take_writer()?;
|
||||
let writer = Arc::new(TokioMutex::new(writer));
|
||||
let writer_handle: JoinHandle<()> = tokio::spawn({
|
||||
let writer = Arc::clone(&writer);
|
||||
@@ -193,6 +237,7 @@ pub async fn spawn_pty_process(
|
||||
});
|
||||
|
||||
let (session, output_rx) = ExecCommandSession::new(
|
||||
master,
|
||||
writer_tx,
|
||||
output_tx,
|
||||
killer,
|
||||
@@ -209,3 +254,87 @@ pub async fn spawn_pty_process(
|
||||
exit_rx,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[tokio::test]
|
||||
#[cfg(target_os = "windows")]
|
||||
async fn spawn_cmd_succeeds() {
|
||||
let mut env: HashMap<String, String> = std::env::vars().collect();
|
||||
if let Some(system_root) = env.get("SystemRoot").cloned() {
|
||||
let base_paths = vec![
|
||||
format!(r"{system_root}\system32"),
|
||||
system_root.clone(),
|
||||
format!(r"{system_root}\System32\Wbem"),
|
||||
format!(r"{system_root}\System32\WindowsPowerShell\v1.0"),
|
||||
];
|
||||
env.insert("PATH".to_string(), base_paths.join(";"));
|
||||
}
|
||||
let cwd = std::env::current_dir().expect("current_dir");
|
||||
eprintln!(
|
||||
"SystemRoot={:?} ComSpec={:?} PATH={:?}",
|
||||
env.get("SystemRoot"),
|
||||
env.get("ComSpec"),
|
||||
env.get("PATH")
|
||||
.map(|p| p.split(';').take(3).collect::<Vec<_>>())
|
||||
);
|
||||
|
||||
let comspec = std::env::var("ComSpec").unwrap_or_else(|_| "cmd.exe".to_string());
|
||||
let mut spawned = spawn_pty_process(
|
||||
&comspec,
|
||||
&["/C".to_string(), "exit 0".to_string()],
|
||||
&cwd,
|
||||
&env,
|
||||
&None,
|
||||
)
|
||||
.await
|
||||
.expect("spawn cmd");
|
||||
|
||||
let mut output_rx = spawned.output_rx;
|
||||
let first_chunk = output_rx.try_recv().ok();
|
||||
eprintln!(
|
||||
"first_chunk = {:?}",
|
||||
first_chunk
|
||||
.as_ref()
|
||||
.map(|bytes| String::from_utf8_lossy(bytes))
|
||||
);
|
||||
|
||||
let status = spawned.exit_rx.await.expect("exit status");
|
||||
assert_eq!(status, 0, "cmd.exe should exit successfully");
|
||||
|
||||
// Drain any output to avoid broadcast warnings.
|
||||
while output_rx.try_recv().is_ok() {}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_os = "windows")]
|
||||
fn spawn_cmd_blocking() {
|
||||
let pty_system = native_pty_system();
|
||||
let mut pair = pty_system
|
||||
.openpty(PtySize {
|
||||
rows: 24,
|
||||
cols: 80,
|
||||
pixel_width: 0,
|
||||
pixel_height: 0,
|
||||
})
|
||||
.expect("open pty");
|
||||
|
||||
let mut cmd = CommandBuilder::new(
|
||||
std::env::var("ComSpec").unwrap_or_else(|_| "C:\\windows\\system32\\cmd.exe".into()),
|
||||
);
|
||||
cmd.arg("/C");
|
||||
cmd.arg("exit 0");
|
||||
|
||||
let mut child = pair.slave.spawn_command(cmd).expect("spawn blocking cmd");
|
||||
drop(pair.slave);
|
||||
|
||||
// Explicitly close stdin so the child can exit cleanly.
|
||||
drop(pair.master.take_writer().expect("writer"));
|
||||
|
||||
let status = child.wait().expect("wait for child");
|
||||
assert_eq!(status.exit_code(), 0, "cmd.exe exit code");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user