From 27867e5af3aea3ca1ce736350f0ed3e5cfae4d4c Mon Sep 17 00:00:00 2001 From: Jillur Rahman Date: Tue, 7 Jul 2026 06:16:39 +0600 Subject: [PATCH] fix(providers): treat recoverable Antigravity/Cloud-Code 403s as project errors, not account bans (#6452) fix(providers): treat recoverable Antigravity/CF 403 as retryable (net +1/-0, test OK). Integrated into release/v3.8.46. --- CHANGELOG.md | 1 + open-sse/services/errorClassifier.ts | 24 ++++++++- .../errorclassifier-antigravity-403.test.ts | 50 +++++++++++++++++++ 3 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 tests/unit/errorclassifier-antigravity-403.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index e3ccaa0a33..f0de0d7130 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ ### 🐛 Bug Fixes +- **fix(providers):** recoverable Antigravity / Cloudflare `403` responses are now classified as retryable instead of terminal, so a transient WAF block no longer bans the connection. Regression guard: `tests/unit/errorclassifier-antigravity-403.test.ts`. (thanks @developerjillur) - **fix(mitm):** `sanitizeHeaders` now redacts `Set-Cookie` response headers so upstream session cookies never leak into logs / diagnostics. Regression guard: `tests/unit/mitm-sanitize-headers.test.ts`. (thanks @developerjillur) - **fix(api):** `/api/compression/preview` now accepts `mode: "caveman"` and correctly handles stacked / zero-compression previews ([#6425](https://github.com/diegosouzapw/OmniRoute/issues/6425)). Regression guard: `tests/unit/api/compression-preview-caveman-and-stacked-6425.test.ts`. (thanks @chirag127) - **feat(providers):** add **Zed** hosted LLM aggregator as a native-app provider ([#6118](https://github.com/diegosouzapw/OmniRoute/pull/6118)) — OAuth sign-in via the Zed hosted flow, registered through the shared provider registry + executor. Regression guards: `tests/unit/zed-oauth-provider.test.ts`, `zed-import-utils.test.ts`, `zed-docker-detect.test.ts`, `mitm-handler-zed.test.ts`. VPS-validated via live operator login (Hard Rule #18). diff --git a/open-sse/services/errorClassifier.ts b/open-sse/services/errorClassifier.ts index acc302858c..fb1f9c2116 100644 --- a/open-sse/services/errorClassifier.ts +++ b/open-sse/services/errorClassifier.ts @@ -158,7 +158,29 @@ export function classifyProviderError( return PROVIDER_ERROR_TYPES.ACCOUNT_DEACTIVATED; } if (statusCode === 403) { - if (bodyStr.includes("has not been used in project")) { + // Cloud Code / Antigravity (Gemini Code Assist) 403s are almost always a + // RECOVERABLE project-config issue — the Cloud AI Companion API not enabled + // on the project ("has not been used in project …", SERVICE_DISABLED, + // accessNotConfigured), a stale/mismatched project, or PERMISSION_DENIED on + // the project — NOT an account ban. Real account bans are already caught by + // isAccountDeactivated above (→ ACCOUNT_DEACTIVATED). Classifying these as + // PROJECT_ROUTE_ERROR keeps the account active and recoverable once the + // project/API is fixed, instead of permanently disabling it on a single + // fixable 403 (which previously required a full OAuth reconnect). (antigravity-403) + const p = (provider || "").toLowerCase(); + const isCloudCodeProvider = + p === "antigravity" || + p === "gemini-cli" || + p.includes("cloudcode") || + p.includes("cloud-code"); + const recoverableProject403 = + bodyStr.includes("has not been used in project") || + bodyStr.includes("SERVICE_DISABLED") || + bodyStr.includes("accessNotConfigured") || + bodyStr.includes("PERMISSION_DENIED") || + /\bit is disabled\b/i.test(bodyStr) || + isCloudCodeProvider; + if (recoverableProject403) { return PROVIDER_ERROR_TYPES.PROJECT_ROUTE_ERROR; } if (provider && getProviderCategory(provider) === "apikey") { diff --git a/tests/unit/errorclassifier-antigravity-403.test.ts b/tests/unit/errorclassifier-antigravity-403.test.ts new file mode 100644 index 0000000000..8cc09e767e --- /dev/null +++ b/tests/unit/errorclassifier-antigravity-403.test.ts @@ -0,0 +1,50 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { + classifyProviderError, + PROVIDER_ERROR_TYPES, +} from "../../open-sse/services/errorClassifier.ts"; + +// A Cloud Code / Antigravity (Gemini Code Assist) 403 is almost always a +// RECOVERABLE project-config problem — the Cloud AI Companion API not enabled on +// the project, a stale project, or PERMISSION_DENIED — NOT an account ban. +// It must classify as PROJECT_ROUTE_ERROR so the account stays usable once the +// project is fixed, instead of being disabled for ~a year like a real ban. + +test("403 'has not been used in project' (antigravity) -> PROJECT_ROUTE_ERROR", () => { + const body = { + error: { + code: 403, + status: "PERMISSION_DENIED", + message: + "Cloud AI Companion API has not been used in project 123 before or it is disabled.", + }, + }; + assert.equal( + classifyProviderError(403, body, "antigravity"), + PROVIDER_ERROR_TYPES.PROJECT_ROUTE_ERROR, + ); +}); + +test("403 SERVICE_DISABLED / PERMISSION_DENIED (gemini-cli) -> PROJECT_ROUTE_ERROR", () => { + const body = { error: { status: "PERMISSION_DENIED", details: [{ reason: "SERVICE_DISABLED" }] } }; + assert.equal( + classifyProviderError(403, body, "gemini-cli"), + PROVIDER_ERROR_TYPES.PROJECT_ROUTE_ERROR, + ); +}); + +test("403 on a cloud-code provider with a bare body -> still recoverable PROJECT_ROUTE_ERROR", () => { + assert.equal( + classifyProviderError(403, "forbidden", "antigravity-cloudcode"), + PROVIDER_ERROR_TYPES.PROJECT_ROUTE_ERROR, + ); +}); + +test("403 real ban signal -> still ACCOUNT_DEACTIVATED (ban detection preserved)", () => { + const body = "This service has been disabled in this account for violation of policy."; + assert.equal( + classifyProviderError(403, body, "antigravity"), + PROVIDER_ERROR_TYPES.ACCOUNT_DEACTIVATED, + ); +});