mirror of
https://github.com/openai/codex.git
synced 2026-11-10 21:00:47 +00:00
Treat workspace access denials as terminal errors (#60480)
## Why A `403` with `codex_workspace_access_denied` indicates that the account cannot use Codex in the selected workspace. Retrying or falling back to HTTP does not resolve that denial. ## What changed Map `codex_workspace_access_denied` and `invalid_workspace_selected` responses to a shared workspace access denial error, and rename the error variant to reflect its broader scope. Keep workspace denials excluded from compaction model fallback. ## Testing Extend API error mapping tests to cover the new code on `403` and preserve ordinary handling for `503`. Add WebSocket coverage verifying that an explicit workspace access denial does not trigger HTTP fallback. GitOrigin-RevId: e601a22a0fa783a4ce5401a98ff9c8d3d9d1b074
This commit is contained in:
@@ -290,9 +290,10 @@ fn map_api_error_details(err: ApiError) -> CodexErr {
|
||||
});
|
||||
let code = body_code.or(response.identity_error_code.as_deref());
|
||||
match (status, code) {
|
||||
(http::StatusCode::FORBIDDEN, Some("invalid_workspace_selected")) => {
|
||||
CodexErrorDetails::InvalidWorkspaceSelected(response).into()
|
||||
}
|
||||
(
|
||||
http::StatusCode::FORBIDDEN,
|
||||
Some("invalid_workspace_selected" | "codex_workspace_access_denied"),
|
||||
) => CodexErrorDetails::WorkspaceAccessDenied(response).into(),
|
||||
(http::StatusCode::FORBIDDEN, Some("access_program_not_enabled")) => {
|
||||
CodexErrorDetails::AccessProgramNotEnabled(ProviderRejection::Http(
|
||||
response,
|
||||
|
||||
@@ -814,12 +814,15 @@ fn only_an_explicit_workspace_403_stops_retries() {
|
||||
use http::StatusCode;
|
||||
let provider = r#"{"error":{"code":"invalid_workspace_selected"}}"#;
|
||||
let detail = r#"{"detail":{"code":"invalid_workspace_selected"}}"#;
|
||||
let access_denied = r#"{"detail":{"code":"codex_workspace_access_denied"}}"#;
|
||||
let other = r#"{"detail":{"code":"route_rate_limited"}}"#;
|
||||
let workspace_header =
|
||||
http::HeaderValue::from_str(&base64::engine::general_purpose::STANDARD.encode(provider))
|
||||
.unwrap();
|
||||
for (status, body, with_header, terminal) in [
|
||||
(StatusCode::FORBIDDEN, detail, false, true),
|
||||
(StatusCode::FORBIDDEN, access_denied, false, true),
|
||||
(StatusCode::SERVICE_UNAVAILABLE, access_denied, false, false),
|
||||
(StatusCode::FORBIDDEN, provider, false, true),
|
||||
(StatusCode::FORBIDDEN, "", true, true),
|
||||
(StatusCode::FORBIDDEN, "", false, false),
|
||||
@@ -845,10 +848,7 @@ fn only_an_explicit_workspace_403_stops_retries() {
|
||||
retry_after: None,
|
||||
}));
|
||||
assert_eq!(
|
||||
matches!(
|
||||
err.details(),
|
||||
CodexErrorDetails::InvalidWorkspaceSelected(_)
|
||||
),
|
||||
matches!(err.details(), CodexErrorDetails::WorkspaceAccessDenied(_)),
|
||||
terminal,
|
||||
"{err}"
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@ pub(crate) fn should_retry_with_current_model(
|
||||
CodexErrorDetails::TurnAborted
|
||||
| CodexErrorDetails::Interrupted
|
||||
| CodexErrorDetails::SessionBudgetExceeded
|
||||
| CodexErrorDetails::InvalidWorkspaceSelected(_)
|
||||
| CodexErrorDetails::WorkspaceAccessDenied(_)
|
||||
| CodexErrorDetails::ContentPolicyViolation { .. }
|
||||
| CodexErrorDetails::ResponseIncomplete(_)
|
||||
) || (previous.model_info().slug == current.model_info().slug
|
||||
|
||||
@@ -129,6 +129,7 @@ async fn websocket_fallback_switches_to_http_after_retries_exhausted() -> Result
|
||||
|
||||
#[test_case::test_case(true, Some("invalid_workspace_selected"), 3, 0; "workspace denial on websocket")]
|
||||
#[test_case::test_case(false, Some("invalid_workspace_selected"), 0, 2; "workspace denial on http")]
|
||||
#[test_case::test_case(true, Some("codex_workspace_access_denied"), 3, 0; "workspace permission denied on websocket")]
|
||||
#[test_case::test_case(true, Some("access_program_not_enabled"), 3, 0; "access program denial on websocket")]
|
||||
#[test_case::test_case(false, Some("access_program_not_enabled"), 0, 2; "access program denial on http")]
|
||||
#[test_case::test_case(true, None, 3, 4; "bare websocket and http denials")]
|
||||
|
||||
@@ -148,9 +148,9 @@ pub enum CodexErrorDetails {
|
||||
/// surface a polite FunctionCallOutput back to the model instead of crashing the CLI.
|
||||
#[error("interrupted (Ctrl-C). Something went wrong? Hit `/feedback` to report the issue.")]
|
||||
Interrupted,
|
||||
/// The server rejected the selected workspace.
|
||||
/// The account cannot use Codex in the selected workspace.
|
||||
#[error("{0}")]
|
||||
InvalidWorkspaceSelected(UnexpectedResponseError),
|
||||
WorkspaceAccessDenied(UnexpectedResponseError),
|
||||
/// The provider denied the access program required for this request.
|
||||
#[error("{0}")]
|
||||
AccessProgramNotEnabled(ProviderRejection),
|
||||
@@ -479,7 +479,7 @@ impl CodexErr {
|
||||
| CodexErrorDetails::FlexUnavailable
|
||||
| CodexErrorDetails::CyberPolicy { .. }
|
||||
| CodexErrorDetails::BioPolicy { .. }
|
||||
| CodexErrorDetails::InvalidWorkspaceSelected(_)
|
||||
| CodexErrorDetails::WorkspaceAccessDenied(_)
|
||||
| CodexErrorDetails::AccessProgramNotEnabled(_)
|
||||
| CodexErrorDetails::MisalignmentPolicyViolation { .. } => None,
|
||||
CodexErrorDetails::ServerOverloaded
|
||||
@@ -556,7 +556,7 @@ impl CodexErr {
|
||||
http_status_code: self.http_status_code_value(),
|
||||
},
|
||||
CodexErrorDetails::ConnectionFailed(_)
|
||||
| CodexErrorDetails::InvalidWorkspaceSelected(_)
|
||||
| CodexErrorDetails::WorkspaceAccessDenied(_)
|
||||
| CodexErrorDetails::AccessProgramNotEnabled(_)
|
||||
| CodexErrorDetails::UnexpectedStatus(_) => CodexErrorInfo::HttpConnectionFailed {
|
||||
http_status_code: self.http_status_code_value(),
|
||||
@@ -604,7 +604,7 @@ impl CodexErr {
|
||||
CodexErrorDetails::ModelNotFound { status, .. }
|
||||
| CodexErrorDetails::ContentPolicyViolation { status, .. } => *status,
|
||||
CodexErrorDetails::RetryLimit(err) => Some(err.status),
|
||||
CodexErrorDetails::InvalidWorkspaceSelected(err)
|
||||
CodexErrorDetails::WorkspaceAccessDenied(err)
|
||||
| CodexErrorDetails::AccessProgramNotEnabled(ProviderRejection::Http(err))
|
||||
| CodexErrorDetails::UnexpectedStatus(err) => Some(err.status),
|
||||
CodexErrorDetails::ConnectionFailed(err) => err.source.status(),
|
||||
|
||||
Reference in New Issue
Block a user