diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 0ce0c4ea..81ec3cab 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -1957,6 +1957,20 @@ async fn handle_sandbox_error( }; } + // similarly, if the command timed out, we can simply return this failure to the model + if matches!(error, SandboxErr::Timeout) { + return ResponseInputItem::FunctionCallOutput { + call_id, + output: FunctionCallOutputPayload { + content: format!( + "command timed out after {} milliseconds", + params.timeout_duration().as_millis() + ), + success: Some(false), + }, + }; + } + // Note that when `error` is `SandboxErr::Denied`, it could be a false // positive. That is, it may have exited with a non-zero exit code, not // because the sandbox denied it, but because that is its expected behavior, diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index dce02cc5..e98aeeae 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -51,6 +51,12 @@ pub struct ExecParams { pub env: HashMap, } +impl ExecParams { + pub fn timeout_duration(&self) -> Duration { + Duration::from_millis(self.timeout_ms.unwrap_or(DEFAULT_TIMEOUT_MS)) + } +} + #[derive(Clone, Copy, Debug, PartialEq)] pub enum SandboxType { None, @@ -83,11 +89,9 @@ pub async fn process_exec_tool_call( { SandboxType::None => exec(params, sandbox_policy, ctrl_c, stdout_stream.clone()).await, SandboxType::MacosSeatbelt => { + let timeout = params.timeout_duration(); let ExecParams { - command, - cwd, - timeout_ms, - env, + command, cwd, env, .. } = params; let child = spawn_command_under_seatbelt( command, @@ -97,14 +101,12 @@ pub async fn process_exec_tool_call( env, ) .await?; - consume_truncated_output(child, ctrl_c, timeout_ms, stdout_stream.clone()).await + consume_truncated_output(child, ctrl_c, timeout, stdout_stream.clone()).await } SandboxType::LinuxSeccomp => { + let timeout = params.timeout_duration(); let ExecParams { - command, - cwd, - timeout_ms, - env, + command, cwd, env, .. } = params; let codex_linux_sandbox_exe = codex_linux_sandbox_exe @@ -120,7 +122,7 @@ pub async fn process_exec_tool_call( ) .await?; - consume_truncated_output(child, ctrl_c, timeout_ms, stdout_stream).await + consume_truncated_output(child, ctrl_c, timeout, stdout_stream).await } }; let duration = start.elapsed(); @@ -255,16 +257,16 @@ pub struct ExecToolCallOutput { } async fn exec( - ExecParams { - command, - cwd, - timeout_ms, - env, - }: ExecParams, + params: ExecParams, sandbox_policy: &SandboxPolicy, ctrl_c: Arc, stdout_stream: Option, ) -> Result { + let timeout = params.timeout_duration(); + let ExecParams { + command, cwd, env, .. + } = params; + let (program, args) = command.split_first().ok_or_else(|| { CodexErr::Io(io::Error::new( io::ErrorKind::InvalidInput, @@ -282,7 +284,7 @@ async fn exec( env, ) .await?; - consume_truncated_output(child, ctrl_c, timeout_ms, stdout_stream).await + consume_truncated_output(child, ctrl_c, timeout, stdout_stream).await } /// Consumes the output of a child process, truncating it so it is suitable for @@ -290,7 +292,7 @@ async fn exec( pub(crate) async fn consume_truncated_output( mut child: Child, ctrl_c: Arc, - timeout_ms: Option, + timeout: Duration, stdout_stream: Option, ) -> Result { // Both stdout and stderr were configured with `Stdio::piped()` @@ -324,7 +326,6 @@ pub(crate) async fn consume_truncated_output( )); let interrupted = ctrl_c.notified(); - let timeout = Duration::from_millis(timeout_ms.unwrap_or(DEFAULT_TIMEOUT_MS)); let exit_status = tokio::select! { result = tokio::time::timeout(timeout, child.wait()) => { match result {