Skip to content
75 changes: 74 additions & 1 deletion codex-rs/core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,27 @@ impl CodexErr {

pub fn get_error_message_ui(e: &CodexErr) -> String {
match e {
CodexErr::Sandbox(SandboxErr::Denied { output }) => output.stderr.text.clone(),
CodexErr::Sandbox(SandboxErr::Denied { output }) => {
let stderr = output.stderr.text.trim();
if !stderr.is_empty() {
return output.stderr.text.clone();
}

let aggregated = output.aggregated_output.text.trim();
if !aggregated.is_empty() {
return output.aggregated_output.text.clone();
}

let stdout = output.stdout.text.trim();
if !stdout.is_empty() {
return output.stdout.text.clone();
}

format!(
"command failed inside sandbox with exit code {}",
output.exit_code
)
}
// Timeouts are not sandbox errors from a UX perspective; present them plainly
CodexErr::Sandbox(SandboxErr::Timeout { output }) => format!(
"error: command timed out after {} ms",
Expand All @@ -318,7 +338,9 @@ pub fn get_error_message_ui(e: &CodexErr) -> String {
#[cfg(test)]
mod tests {
use super::*;
use crate::exec::StreamOutput;
use codex_protocol::protocol::RateLimitWindow;
use pretty_assertions::assert_eq;

fn rate_limit_snapshot() -> RateLimitSnapshot {
RateLimitSnapshot {
Expand Down Expand Up @@ -348,6 +370,57 @@ mod tests {
);
}

#[test]
fn sandbox_denied_prefers_stderr_when_available() {
let output = ExecToolCallOutput {
exit_code: 123,
stdout: StreamOutput::new("stdout text".to_string()),
stderr: StreamOutput::new("stderr detail".to_string()),
aggregated_output: StreamOutput::new("aggregated text".to_string()),
duration: Duration::from_millis(1),
timed_out: false,
};
let err = CodexErr::Sandbox(SandboxErr::Denied {
output: Box::new(output),
});
assert_eq!(get_error_message_ui(&err), "stderr detail");
}

#[test]
fn sandbox_denied_uses_aggregated_output_when_stderr_empty() {
let output = ExecToolCallOutput {
exit_code: 77,
stdout: StreamOutput::new(String::new()),
stderr: StreamOutput::new(String::new()),
aggregated_output: StreamOutput::new("aggregate detail".to_string()),
duration: Duration::from_millis(10),
timed_out: false,
};
let err = CodexErr::Sandbox(SandboxErr::Denied {
output: Box::new(output),
});
assert_eq!(get_error_message_ui(&err), "aggregate detail");
}

#[test]
fn sandbox_denied_reports_exit_code_when_no_output_available() {
let output = ExecToolCallOutput {
exit_code: 13,
stdout: StreamOutput::new(String::new()),
stderr: StreamOutput::new(String::new()),
aggregated_output: StreamOutput::new(String::new()),
duration: Duration::from_millis(5),
timed_out: false,
};
let err = CodexErr::Sandbox(SandboxErr::Denied {
output: Box::new(output),
});
assert_eq!(
get_error_message_ui(&err),
"command failed inside sandbox with exit code 13"
);
}

#[test]
fn usage_limit_reached_error_formats_free_plan() {
let err = UsageLimitReachedError {
Expand Down
17 changes: 17 additions & 0 deletions codex-rs/core/src/executor/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,23 @@ mod tests {
assert_eq!(message, "failed in sandbox: sandbox stderr");
}

#[test]
fn sandbox_failure_message_falls_back_to_aggregated_output() {
let output = ExecToolCallOutput {
exit_code: 101,
stdout: StreamOutput::new(String::new()),
stderr: StreamOutput::new(String::new()),
aggregated_output: StreamOutput::new("aggregate text".to_string()),
duration: Duration::from_millis(10),
timed_out: false,
};
let err = SandboxErr::Denied {
output: Box::new(output),
};
let message = sandbox_failure_message(err);
assert_eq!(message, "failed in sandbox: aggregate text");
}

#[test]
fn normalize_function_error_synthesizes_payload() {
let err = FunctionCallError::RespondToModel("boom".to_string());
Expand Down
Loading