Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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