apply_patch sandbox retry can request duplicate approval and hang app-server turns
Summary
apply_patch can request a second approval during the sandbox retry path even after the patch was already config/auto-approved. In app-server or wrapper contexts where that internal retry approval is not answered, the turn can wait indefinitely without producing a tool result or turn/completed.
I have a tested fix on this branch/commit:
- Branch: https://github.com/mzhaom/codex/tree/fix/apply-patch-retry-approval
- Commit: https://github.com/mzhaom/codex/commit/5ebb6d0de103f628854cc0cce3b5892d4ae0dfb8
- Compare: https://github.com/openai/codex/compare/main...mzhaom:fix/apply-patch-retry-approval
I could not open a PR because this account/token is blocked from CreatePullRequest against openai/codex, even though mzhaom/codex is a real fork and the branch is comparable.
Observed behavior
A run got stuck after Codex core accepted and internally dispatched an apply_patch freeform custom tool call. The model response completed and Codex logged the tool as config-approved, but there was no corresponding custom_tool_call_output, no final assistant message, and no turn/completed event.
The target worktree remained clean and the expected patched file was not created, so this was not just client-side rendering or wrapper output handling.
Root cause
Tracing the core tool path showed:
- The Responses API emitted a freeform
custom_tool_callforapply_patch. - Codex core accepted it and routed it through
ToolRouter::dispatch_tool_call_with_code_mode_resultintoApplyPatchHandler. apply_patch::apply_patchassessed the patch as safe and returnedExecApprovalRequirement::Skip.ToolOrchestratorlogged the tool decision as config-approved.- The first sandboxed attempt can fail when the active workspace-write policy does not allow the target write.
ApplyPatchRuntimeallows a no-sandbox retry underAskForApproval::OnRequest.- The orchestrator did not remember the earlier
Skipdecision asalready_approved, so the retry path requested a second approval. - In app-server/wrapper contexts that do not answer that retry approval, the tool call never produces a result and the turn never completes.
The bug is the lost approval state between the initial config-approved Skip and the later sandbox retry decision.
Fix on branch
The branch adds a small explicit hook to the Approvable trait:
fn skip_requirement_counts_as_approval(&self, _req: &Req) -> bool {
false
}
The default is false, preserving current behavior for other tools. ApplyPatchRuntime opts in because its upstream safety assessment has already approved the patch operation before execution is delegated to the runtime.
ToolOrchestrator now sets already_approved from that hook in the non-guardian ExecApprovalRequirement::Skip path. That lets apply_patch retry outside the sandbox without emitting a duplicate approval request.
Changed files:
codex-rs/core/src/tools/sandboxing.rscodex-rs/core/src/tools/orchestrator.rscodex-rs/core/src/tools/runtimes/apply_patch.rscodex-rs/core/src/session/tests.rs
Repro test
The branch adds config_approved_tool_retry_does_not_request_second_approval, which uses a fake runtime that:
- is config-approved via
ExecApprovalRequirement::Skip - fails the first sandboxed attempt with
SandboxErr::Denied - allows a no-sandbox retry
- counts approval requests
With only the orchestrator propagation line removed, the test fails exactly as expected:
assertion failed: `(left == right)`
left: 1
right: 0
That proves the original behavior requested one unexpected second approval. With the fix restored, the same test passes.
Validation run
Local validation:
cargo fmt --package codex-core
cargo test -p codex-core config_approved_tool_retry_does_not_request_second_approval
cargo test -p codex-core apply_patch
cargo test -p codex-app-server turn_start_does_not_stream_apply_patch_change_updates_without_feature_v2
git diff --check
Results:
- focused regression test passed with the fix
- pre-fix reproduction failed with one unexpected retry approval
cargo test -p codex-core apply_patchpassed: 293 tests- app-server apply_patch turn completion test passed
git diff --checkpassed
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗