Guardian auto-review can select a non-servable default model id for custom providers, causing fail-closed denials
Summary
When approvals_reviewer = "auto_review" (the "Approve for me" / guardian auto-review flow) is used against a custom model_provider (observed with Azure OpenAI, wire_api = "responses", own env_key, not requires_openai_auth), the guardian reviewer session can select the default preferred review model id (codex-auto-review) even though that id isn't a real, servable model on the active provider. The resulting request 404s, the guardian review is treated as failed, and — because the guardian fails closed on any review error — the underlying user action (e.g. a sandbox escalation to run a command) is denied as "unacceptable risk," even though the actual cause is an unrelated backend 404, not a risk assessment.
Environment
- codex-rs built from
main model_provider= custom Azure OpenAI provider (wire_api = "responses", ownenv_key, notrequires_openai_auth)[features] approvals_reviewer = "auto_review"(Desktop app "Approve for me")- Trigger: any action that requires sandbox escalation approval (e.g. running a command that needs network/dependency access outside the sandbox)
Observed behavior
The command is blocked before it runs, and the tool output / assistant message surfaces:
Rejected("This action was rejected due to unacceptable risk.
Reason: Automatic approval review failed: unexpected status 404 Not Found: The API deployment for this resource does not exist. If you created the deployment within the last 5 minutes, please wait a moment and try again., url: https://<resource>.openai.azure.com/openai/v1/responses
The agent must not attempt to achieve the same outcome via workaround, indirect execution, or policy circumvention. Proceed only with a materially safer alternative, or if the user explicitly approves the action after being informed of the risk. Otherwise, stop and request user input.")
The <resource> deployment referenced in the URL does not have (and was never expected to have) a deployment literally named codex-auto-review — the active session's real model is a different, correctly-deployed model id.
Root cause
codex-rs/core/src/guardian/review.rs, guardian_review_session_config:
let default_review_model_id = turn.provider.approval_review_preferred_model(); // "codex-auto-review" unless overridden
let model_override = turn.model_info.auto_review_model_override.as_deref();
let review_model_id = model_override.unwrap_or(default_review_model_id);
let review_model = available_models
.iter()
.find(|preset| preset.model == review_model_id);
...
let (guardian_model, guardian_reasoning_effort) = if let Some(preset) = review_model {
// uses `review_model_id` (e.g. literal "codex-auto-review") verbatim
(review_model_id.to_string(), reasoning_effort)
} else {
// falls back to the active session's real model
(model_override.unwrap_or(turn.model_info.slug.as_str()).to_string(), reasoning_effort)
};
The intent is clearly to fall back to the active session model when the preferred review model isn't available. In practice this fallback doesn't trigger for custom providers, because available_models (ModelsManager::list_models) is documented as "filtered by auth mode and visibility" — not by whether the specific active provider can actually serve a given model id. codex-auto-review is a legitimate, visible catalog entry for API-key-auth in general, so available_models.iter().find(...) succeeds even when the active provider is a custom Azure deployment that has no such model. The code then sends that literal (unservable) model id to the provider, gets a 404, and — combined with the guardian's fail-closed error handling — denies the underlying action instead of surfacing the real cause.
This is a different root cause from #31718 (which is about codex-client not retrying a transient 400), but has a related symptom class: an Azure-backed custom provider hitting an avoidable request failure inside the guardian/auto-review path.
Suggested direction (not a PR — happy to send one if invited)
Only trust the catalog match for the default preferred review model when the provider can actually serve OpenAI-hosted-only models (same signal already used elsewhere, e.g. image_generation_runtime_enabled in codex-rs/core/src/tools/spec_plan.rs, for gating codex-auto-review-style hosted-only capabilities). An explicit auto_review_model_override should still always be honored/trusted as-is.
let model_override = turn.model_info.auto_review_model_override.as_deref();
let review_model_id = model_override.unwrap_or(default_review_model_id);
let provider_can_serve_preferred_review_model = model_override.is_some()
|| turn.provider.info().uses_openai_actor_authorization()
|| (turn.provider.info().requires_openai_auth
&& turn
.auth_manager
.as_deref()
.is_some_and(AuthManager::current_auth_uses_codex_backend));
let review_model = provider_can_serve_preferred_review_model
.then(|| {
available_models
.iter()
.find(|preset| preset.model == review_model_id)
})
.flatten();
With this change, custom providers always fall through to the active session's real model for the guardian reviewer, instead of intermittently picking an id that only exists in the general catalog. Verified locally: sandbox escalation approvals that previously failed closed with the 404 above now succeed against the same Azure provider.
Open questions for the team:
- Whether
ModelsManager::list_models/the underlying catalog should instead be scoped per-provider (would fix this class of bug at the source rather than needing per-call-site guards like this one and the existingimage_generation_runtime_enabledcheck). - Whether there are other call sites that trust a catalog match without checking provider auth type the way
image_generation_runtime_enableddoes (e.g.memory_extraction_preferred_model/memory_consolidation_preferred_modelfollow the sameProviderCapabilities-style pattern and may have the same issue).
Happy to share full repro steps or open a PR along these lines if that's useful.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗