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:
Charlie Marsh
2026-11-10 00:54:30 +00:00
committed by copyberry
parent 65f2709052
commit 4b345a8de9
5 changed files with 15 additions and 13 deletions
+4 -3
View File
@@ -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,
+4 -4
View File
@@ -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}"
);