fix(providers): a Google Cloud Code / Antigravity 403 VALIDATION_REQUIRED no longer permanently bans the connection (#14848)

Validated on the 2026-09-28 release-drain board. Maintainer rework on top of your commits: merged the current release/v3.8.51 tip and folded the explanatory comment into the list so open-sse/services/accountFallback.ts stays within its frozen file-size ceiling (2520 -> 2516, no behavior change). antigravity-verify-account-403-not-a-ban.test.ts 10/10, typecheck:core clean, file-size gate OK. Thank you @adevwithpurpose!
This commit is contained in:
adevwithpurpose 2026-09-29 06:08:47 +05:00 • committed by GitHub
parent 29606e2b91
commit 3a2678f83b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 215 additions and 7 deletions

View file

@ -0,0 +1 @@
- **fix(providers):** A Google Cloud Code / Antigravity `403 VALIDATION_REQUIRED` ("Verify your account to continue") no longer permanently bans the connection — the phrase was in `ACCOUNT_DEACTIVATED_SIGNALS`, so a single transient occurrence parked a healthy, fully-quota'd account as `banned` (`permanent: true`, 1-year cooldown, never auto-recovers). Measured on a live deployment: one Antigravity connection returned 33 of these 403s inside 10 minutes and stayed `active`, while a sibling holding 100 % of its quota on all 17 windows was banned by exactly one — the only difference being which attempt happened to be served. The phrase now lives in a new non-terminal `ACCOUNT_VERIFICATION_REQUIRED_SIGNALS` table (`open-sse/services/errorClassifier.ts`, home of the 403 branch that consumes it) and classifies as recoverable `PROJECT_ROUTE_ERROR`, which also un-blocks the cloud-code project-route recovery added for Gemini Code Assist (#868, #6452) — it was unreachable for this wording because `accountDeactivated` is evaluated first. Genuine Antigravity ban wording (`this service has been disabled in this account for violation`) still terminalizes, and a body carrying both still bans.

View file

@ -17,11 +17,14 @@ state itself is part of the resilience model — see
[RESILIENCE_GUIDE](../architecture/RESILIENCE_GUIDE.md) ("Terminal states").
**Source of truth:** `open-sse/services/accountFallback.ts`
(`ACCOUNT_DEACTIVATED_SIGNALS`, `getMergedBannedSignals()`, `isAccountDeactivated()`).
(`ACCOUNT_DEACTIVATED_SIGNALS`, `getMergedBannedSignals()`, `isAccountDeactivated()`),
plus `open-sse/services/errorClassifier.ts` for the non-terminal verification class
(`ACCOUNT_VERIFICATION_REQUIRED_SIGNALS` / `isAccountVerificationRequired()`) and for
the 403 branch that consumes it.
## Built-in keywords
These 8 substrings always apply (case-insensitive), regardless of any custom list:
These 7 substrings always apply (case-insensitive), regardless of any custom list:
```
account_deactivated
@ -29,7 +32,6 @@ account has been deactivated
account has been disabled
your account has been suspended
this account is deactivated
verify your account to continue (Antigravity / Google Cloud Code)
this service has been disabled in this account for violation (Antigravity)
this service has been disabled in this account (Antigravity)
```
@ -38,12 +40,35 @@ this service has been disabled in this account (Antigravity)
> copy is `ACCOUNT_DEACTIVATED_SIGNALS` in `open-sse/services/accountFallback.ts`;
> treat the block above as a snapshot.
Two adjacent, **separate** signal tables live in the same file and are _not_ part
of banned-keyword detection:
### Not a ban: operator-actionable verification prompts
`verify your account to continue` **used to be** in the list above. It is not a ban
signal and now lives in `ACCOUNT_VERIFICATION_REQUIRED_SIGNALS`, which classifies as
recoverable `PROJECT_ROUTE_ERROR` rather than terminalizing the connection.
Google Cloud Code / Antigravity return it as `403 VALIDATION_REQUIRED`. It is
**transient and fires on healthy, fully-quota'd accounts** — measured on a live
deployment (2026-09-25, `proxy_logs`): one Antigravity connection returned 33 of these
403s inside 10 minutes and stayed `active`, while a sibling connection holding 100 % of
its quota on all 17 windows was permanently banned by a **single** one. The only
difference was which attempt happened to be served.
The distinction matters because a terminal match is `permanent: true` (1-year cooldown,
never auto-recovers), whereas the operator clears a verification prompt in a browser.
Keeping the phrase in the ban list also made the recoverable cloud-code 403 branch in
`classifyProviderError` unreachable for this wording, because `accountDeactivated` is
evaluated first — so the project-route recovery added for Gemini Code Assist in
[#868](https://github.com/diegosouzapw/OmniRoute/pull/868) and
[#6452](https://github.com/diegosouzapw/OmniRoute/pull/6452) could never run.
Three adjacent, **separate** signal tables are _not_ part of banned-keyword detection:
- `CREDITS_EXHAUSTED_SIGNALS` — billing/quota depleted (`insufficient_quota`,
`credit_balance_too_low`, `payment required`, …) → terminal `credits_exhausted`.
- `OAUTH_INVALID_TOKEN_SIGNALS` — **non-terminal**; a token refresh can recover.
- `ACCOUNT_VERIFICATION_REQUIRED_SIGNALS` — **non-terminal**; the operator has to
re-verify the account upstream. Lives in `open-sse/services/errorClassifier.ts`
(the other two live in `accountFallback.ts`). See the section above.
Note: common transient phrases like **`rate limit`** / `429` are handled by the
rate-limit / connection-cooldown path and are **not** ban signals.

View file

@ -216,8 +216,8 @@ export const ACCOUNT_DEACTIVATED_SIGNALS = [
"account has been disabled",
"your account has been suspended",
"this account is deactivated",
// AG (Antigravity/Google Cloud Code) permanent ban signals
"verify your account to continue",
// AG (Antigravity/Google Cloud Code) permanent ban signals. "verify your account to continue" is NOT
// a ban (operator-actionable) — see ACCOUNT_VERIFICATION_REQUIRED_SIGNALS in errorClassifier.ts.
"this service has been disabled in this account for violation",
"this service has been disabled in this account",
];

View file

@ -155,6 +155,27 @@ export function isContextOverflow(errorText: string): boolean {
return CONTEXT_OVERFLOW_REGEX.test(String(errorText || ""));
}
// Operator-actionable account-state prompts — the account is NOT dead, the human
// behind it must complete an upstream verification step. Deliberately NOT part of
// accountFallback.ts's ACCOUNT_DEACTIVATED_SIGNALS: a match there is terminal
// (`permanent: true`, 1-year cooldown, never auto-recovers), which is the wrong
// response to a condition the operator clears in a browser.
//
// Google Cloud Code / Antigravity answer `403 VALIDATION_REQUIRED` with
// "Verify your account to continue". It is TRANSIENT and fires on healthy,
// fully-quota'd accounts. Measured on a live deployment (2026-09-25): one
// Antigravity connection returned 33 of these 403s inside 10 minutes and stayed
// `active`, while a sibling holding 100 % of its quota on all 17 windows was
// permanently banned by a SINGLE one — the only difference being which attempt
// happened to be served. Keeping the phrase in the ban list also shadowed the
// cloud-code recovery below, which is only reached after `accountDeactivated`.
export const ACCOUNT_VERIFICATION_REQUIRED_SIGNALS = ["verify your account to continue"];
export function isAccountVerificationRequired(errorText: string): boolean {
const lower = String(errorText || "").toLowerCase();
return ACCOUNT_VERIFICATION_REQUIRED_SIGNALS.some((sig) => lower.includes(sig));
}
// Matches phrasing like `Model minimax-m3-free is not supported` or
// `model "gpt-9" is not supported` — free-tier/aggregator providers name the
// specific model in the sentence instead of using a fixed fragment like
@ -463,6 +484,22 @@ export function classifyProviderError(
if (statusCode === 403 && accountDeactivated) {
return PROVIDER_ERROR_TYPES.ACCOUNT_DEACTIVATED;
}
if (statusCode === 403 && isAccountVerificationRequired(bodyStr)) {
// Google Cloud Code / Antigravity answer `403 VALIDATION_REQUIRED` with
// "Verify your account to continue". This is a TRANSIENT, operator-actionable
// account-state prompt — not a permanent ban. Measured on a live deployment
// (2026-09-25): one healthy Antigravity connection returned 33 of these 403s
// inside 10 minutes and stayed `active`, while a sibling connection holding
// 100 % of its quota on all 17 windows was permanently banned by a SINGLE one.
// The account serves traffic again as soon as the operator re-verifies, so the
// 1-year `permanent` cooldown ACCOUNT_DEACTIVATED implies is the wrong
// response. Recoverable, like the operator-fixable 403s below — and this
// branch is also what stops a NON-cloud-code provider from falling through to
// the generic FORBIDDEN below, which would ban it on the first occurrence.
// Checked AFTER accountDeactivated so a body carrying a real ban phrase
// ("this service has been disabled in this account for violation") still wins.
return PROVIDER_ERROR_TYPES.PROJECT_ROUTE_ERROR;
}
if (
statusCode === 403 &&
isAnthropicOAuthProvider(provider) &&
@ -662,5 +699,12 @@ export function classifyFakeSuccessBody(
) {
return PROVIDER_ERROR_TYPES.ACCOUNT_DEACTIVATED;
}
// Kept in step with classifyProviderError's 403 branch: a disguised
// "verify your account" prompt is recoverable, not a dead account. Without
// this the phrase would now fall through to `null` here, because it left
// ACCOUNT_DEACTIVATED_SIGNALS.
if (isAccountVerificationRequired(lower)) {
return PROVIDER_ERROR_TYPES.PROJECT_ROUTE_ERROR;
}
return null;
}

View file

@ -0,0 +1,138 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
classifyProviderError,
isAccountVerificationRequired,
PROVIDER_ERROR_TYPES,
} from "../../open-sse/services/errorClassifier.ts";
import {
checkFallbackError,
isAccountDeactivated,
} from "../../open-sse/services/accountFallback.ts";
import { resolveTerminalConnectionStatus } from "../../src/sse/services/authTerminalStatus.ts";
// Google Cloud Code / Antigravity answer `403 VALIDATION_REQUIRED` with
// "Verify your account to continue". That phrase was listed in
// ACCOUNT_DEACTIVATED_SIGNALS, so a SINGLE occurrence permanently banned the
// connection (`permanent: true`, 1-year cooldown, never auto-recovers).
//
// It is a TRANSIENT, operator-actionable prompt, not a ban. Measured on a live
// deployment (2026-09-25, `proxy_logs`): one Antigravity connection returned 33
// of these 403s inside 10 minutes and stayed `active`, while a sibling carrying
// 100 % of its quota on all 17 windows was banned by exactly ONE. The difference
// was only which attempt happened to be served — the phrase carries no
// information about account health.
//
// Keeping it in the terminal list also made the recoverable cloud-code 403
// branch in classifyProviderError unreachable for this wording, because
// `accountDeactivated` is evaluated first.
const LIVE_WIRE_TEXT = "[403]: Antigravity upstream error (403): Verify your account to continue.";
test("403 'Verify your account to continue' (antigravity) -> recoverable, NOT ACCOUNT_DEACTIVATED", () => {
assert.equal(
classifyProviderError(403, LIVE_WIRE_TEXT, "antigravity"),
PROVIDER_ERROR_TYPES.PROJECT_ROUTE_ERROR
);
});
test("403 verification prompt on a NON-cloud-code provider -> recoverable, not FORBIDDEN", () => {
// Guards the generic 403 fall-through: dropping the phrase from the ban list
// must not let any provider be banned permanently by it instead.
assert.equal(
classifyProviderError(403, "Verify your account to continue", "some-other-provider"),
PROVIDER_ERROR_TYPES.PROJECT_ROUTE_ERROR
);
});
test("403 verification prompt is matched mid-body and case-insensitively", () => {
const body = {
error: { code: 403, message: "VALIDATION_REQUIRED: VERIFY YOUR ACCOUNT TO CONTINUE" },
};
assert.equal(
classifyProviderError(403, body, "antigravity"),
PROVIDER_ERROR_TYPES.PROJECT_ROUTE_ERROR
);
});
test("the phrase is no longer a deactivation signal at the predicate level", () => {
assert.equal(isAccountDeactivated(LIVE_WIRE_TEXT), false);
assert.equal(isAccountVerificationRequired(LIVE_WIRE_TEXT), true);
});
test("REGRESSION: a real Antigravity ban phrase still bans", () => {
assert.equal(
classifyProviderError(
403,
"This service has been disabled in this account for violation of policy.",
"antigravity"
),
PROVIDER_ERROR_TYPES.ACCOUNT_DEACTIVATED
);
assert.equal(
classifyProviderError(403, "account_deactivated", "antigravity"),
PROVIDER_ERROR_TYPES.ACCOUNT_DEACTIVATED
);
});
test("REGRESSION: a real ban phrase wins over a co-occurring verification prompt", () => {
const body =
"Verify your account to continue. This service has been disabled in this account for violation.";
assert.equal(
classifyProviderError(403, body, "antigravity"),
PROVIDER_ERROR_TYPES.ACCOUNT_DEACTIVATED
);
});
test("checkFallbackError no longer returns a permanent 1-year lockout for the prompt", () => {
const result = checkFallbackError(403, LIVE_WIRE_TEXT, 0, null, "antigravity");
assert.notEqual(result.permanent, true);
assert.ok(
result.cooldownMs < 365 * 24 * 60 * 60 * 1000,
`expected a transient cooldown, got ${result.cooldownMs}ms`
);
});
test("checkFallbackError still returns a permanent lockout for a real ban", () => {
const result = checkFallbackError(
403,
"This service has been disabled in this account for violation of policy.",
0,
null,
"antigravity"
);
assert.equal(result.permanent, true);
});
test("END-TO-END: the live refusal chain no longer parks the connection as banned", () => {
// Reproduces exactly what the live gateway did: classify the 403 body, run the
// fallback policy, then resolve the terminal status the way auth.ts does.
const errorType = classifyProviderError(403, LIVE_WIRE_TEXT, "antigravity");
const fallback = checkFallbackError(403, LIVE_WIRE_TEXT, 0, null, "antigravity");
const terminal = resolveTerminalConnectionStatus(
403,
{ permanent: fallback.permanent },
errorType,
"antigravity",
false,
LIVE_WIRE_TEXT,
"verify-account-test-connection"
);
assert.equal(terminal, null, `expected no terminal status, got ${terminal}`);
});
test("END-TO-END: a real ban still parks the connection as banned", () => {
const banText = "This service has been disabled in this account for violation of policy.";
const errorType = classifyProviderError(403, banText, "antigravity");
const fallback = checkFallbackError(403, banText, 0, null, "antigravity");
const terminal = resolveTerminalConnectionStatus(
403,
{ permanent: fallback.permanent },
errorType,
"antigravity",
false,
banText,
"real-ban-test-connection"
);
assert.equal(terminal, "banned");
});