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:
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_call for apply_patch.
- Codex core accepted it and routed it through
ToolRouter::dispatch_tool_call_with_code_mode_result into ApplyPatchHandler.
apply_patch::apply_patch assessed the patch as safe and returned ExecApprovalRequirement::Skip.
ToolOrchestrator logged the tool decision as config-approved.
- The first sandboxed attempt can fail when the active workspace-write policy does not allow the target write.
ApplyPatchRuntime allows a no-sandbox retry under AskForApproval::OnRequest.
- The orchestrator did not remember the earlier
Skip decision as already_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.rs
codex-rs/core/src/tools/orchestrator.rs
codex-rs/core/src/tools/runtimes/apply_patch.rs
codex-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_patch passed: 293 tests
- app-server apply_patch turn completion test passed
git diff --check passed
Summary
apply_patchcan 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 orturn/completed.I have a tested fix on this branch/commit:
I could not open a PR because this account/token is blocked from
CreatePullRequestagainstopenai/codex, even thoughmzhaom/codexis a real fork and the branch is comparable.Observed behavior
A run got stuck after Codex core accepted and internally dispatched an
apply_patchfreeform custom tool call. The model response completed and Codex logged the tool as config-approved, but there was no correspondingcustom_tool_call_output, no final assistant message, and noturn/completedevent.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:
custom_tool_callforapply_patch.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.ApplyPatchRuntimeallows a no-sandbox retry underAskForApproval::OnRequest.Skipdecision asalready_approved, so the retry path requested a second approval.The bug is the lost approval state between the initial config-approved
Skipand the later sandbox retry decision.Fix on branch
The branch adds a small explicit hook to the
Approvabletrait:The default is
false, preserving current behavior for other tools.ApplyPatchRuntimeopts in because its upstream safety assessment has already approved the patch operation before execution is delegated to the runtime.ToolOrchestratornow setsalready_approvedfrom that hook in the non-guardianExecApprovalRequirement::Skippath. That letsapply_patchretry 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.rsRepro test
The branch adds
config_approved_tool_retry_does_not_request_second_approval, which uses a fake runtime that:ExecApprovalRequirement::SkipSandboxErr::DeniedWith only the orchestrator propagation line removed, the test fails exactly as expected:
That proves the original behavior requested one unexpected second approval. With the fix restored, the same test passes.
Validation run
Local validation:
Results:
cargo test -p codex-core apply_patchpassed: 293 testsgit diff --checkpassed