mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-10-21 04:54:04 +00:00
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:
parent
29606e2b91
commit
3a2678f83b
5 changed files with 215 additions and 7 deletions
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
138
tests/unit/antigravity-verify-account-403-not-a-ban.test.ts
Normal file
138
tests/unit/antigravity-verify-account-403-not-a-ban.test.ts
Normal 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");
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue