This commit is contained in:
kevin zhao
2025-12-03 18:01:15 -08:00
parent e7c8a14457
commit 16faa24118

View File

@@ -120,50 +120,81 @@ impl ToolOrchestrator {
codex_linux_sandbox_exe: turn_ctx.codex_linux_sandbox_exe.as_ref(),
};
let mut out = tool.run(req, &initial_attempt, tool_ctx).await;
match tool.run(req, &initial_attempt, tool_ctx).await {
Ok(out) => Ok(out),
Err(ToolError::Codex(CodexErr::Sandbox(SandboxErr::Denied { output }))) => {
if !tool.escalate_on_failure() {
return Err(ToolError::Codex(CodexErr::Sandbox(SandboxErr::Denied {
output,
})));
}
if matches!(initial_sandbox, crate::exec::SandboxType::None) {
// If we already skipped sandboxing on the first attempt, there's no
// fallback path.
return out;
}
if !tool.wants_no_sandbox_approval(approval_policy) {
return Err(ToolError::Codex(CodexErr::Sandbox(SandboxErr::Denied {
output,
})));
}
// 3) Retry without sandbox on approval.
if matches!(&out, Err(ToolError::Codex(CodexErr::Sandbox(_))))
&& tool.sandbox_preference() != crate::tools::sandboxing::SandboxablePreference::Require
&& tool.escalate_on_failure()
&& tool.should_bypass_approval(approval_policy, already_approved)
{
// Attempt a retry without sandbox.
out = tool.run(
req,
&SandboxAttempt {
if !tool.should_bypass_approval(approval_policy, already_approved) {
let mut risk = None;
if let Some(metadata) = req.sandbox_retry_data() {
let err = SandboxErr::Denied {
output: output.clone(),
};
let friendly = get_error_message_ui(&CodexErr::Sandbox(err));
let failure_summary = format!("failed in sandbox: {friendly}");
risk = tool_ctx
.session
.assess_sandbox_command(
turn_ctx,
&tool_ctx.call_id,
&metadata.command,
Some(failure_summary.as_str()),
)
.await;
}
let reason_msg = build_denial_reason_from_output(output.as_ref());
let approval_ctx = ApprovalCtx {
session: tool_ctx.session,
turn: turn_ctx,
call_id: &tool_ctx.call_id,
retry_reason: Some(reason_msg),
risk,
};
let decision = tool.start_approval_async(req, approval_ctx).await;
otel.tool_decision(otel_tn, otel_ci, &decision, otel_user);
match decision {
ReviewDecision::Denied | ReviewDecision::Abort => {
return Err(ToolError::Rejected("rejected by user".to_string()));
}
ReviewDecision::Approved
| ReviewDecision::ApprovedExecpolicyAmendment { .. }
| ReviewDecision::ApprovedForSession => {}
}
}
let escalated_attempt = SandboxAttempt {
sandbox: crate::exec::SandboxType::None,
policy: &crate::protocol::SandboxPolicy::DangerFullAccess,
policy: &turn_ctx.sandbox_policy,
manager: &self.sandbox,
sandbox_cwd: &turn_ctx.cwd,
codex_linux_sandbox_exe: turn_ctx.codex_linux_sandbox_exe.as_ref(),
},
tool_ctx,
)
.await;
if let Err(ToolError::Codex(CodexErr::Sandbox(SandboxErr::Denied { output }))) = &out {
return Err(ToolError::Rejected(format!(
"sandbox denied the command, even after approving it without sandbox: {}",
get_error_message_ui(output)
)));
codex_linux_sandbox_exe: None,
};
tool.run(req, &escalated_attempt, tool_ctx).await
}
}
out
}
/// Translate result from tool runner to library level result (errors not in ToolError become CodexErr).
pub fn translate_response(result: Result<ExecToolCallOutput, ToolError>) -> Result<(), CodexErr> {
match result {
Ok(_) => Ok(()),
Err(ToolError::Codex(err)) => Err(err),
Err(ToolError::Rejected(reason)) => Err(CodexErr::Rejected(reason)),
other => other,
}
}
}
fn build_denial_reason_from_output(_output: &ExecToolCallOutput) -> String {
// Keep approval reason terse and stable for UX/tests, but accept the
// output so we can evolve heuristics later without touching call sites.
"command failed; retry without sandbox?".to_string()
}