Files
OmniRoute/src/lib/oauth/antigravityProjectGate.ts
Bob.Hou 05490304bf fix(oauth): mark empty Antigravity projectId as degraded and clear stale errors (#11284) (#12205)
* fix(oauth): mark empty Antigravity projectId as degraded (#11284)

The #11284 gate only fired when projectDiscoveryOutcome was set. Paste
credentials, persistOAuthConnection, and agy CLI import could persist
projectId="" as testStatus=active, so the dashboard showed Connected
while fetchAvailableModels returned 403.

Degrade on empty projectId itself. Keep the refresh token stored so
request-time bootstrap can still self-heal.

Signed-off-by: Minxi Hou <houminxi@gmail.com>

* fix(oauth): clear stale degrade fields and bind CLI imports to builtin client

persistOAuthConnection left errorCode/lastError on the row when a later
connect discovered a Cloud Code projectId. agy CLI import also kept a
leftover custom: oauthClient marker from dashboard OAuth, so the next
refresh hit the operator web client instead of the public desktop client.

Signed-off-by: Minxi Hou <houminxi@gmail.com>

* fix(oauth): null error fields on healthy create paths too

Update already cleared errorCode/lastError* when a projectId appeared.
Create payloads still omitted the keys; match the update shape so a
fresh row cannot keep a leftover degrade marker.

Signed-off-by: Minxi Hou <houminxi@gmail.com>

* test(oauth): pin healthy create/upsert nulling of degrade fields

Forge flagged create payloads omitting errorCode/lastError* when a
projectId is present. Production already writes explicit nulls; the
reader strips them via cleanNulls, so pin both the payload shape and
the upsert path that must overwrite a leftover degrade marker.

Signed-off-by: Minxi Hou <houminxi@gmail.com>

* fix(oauth): type create payload from AntigravityDegradedProjectState

The persistence helper duplicated a subset of the degrade type and
dropped warning. Align the parameter so the HTTP-only warning field
cannot drift from the exported type.

Signed-off-by: Minxi Hou <houminxi@gmail.com>

* fix(oauth): persist degrade status through a single override helper

OAuth exchange/poll-callback spread the whole degrade object, which
wrote warning into the SQLite row and left healthy updates as {}.
Centralize testStatus/errorCode/lastError* so they always win over a
spread tokenData payload.

Signed-off-by: Minxi Hou <houminxi@gmail.com>

---------

Signed-off-by: Minxi Hou <houminxi@gmail.com>
2026-09-01 00:51:18 -03:00

110 lines
4.2 KiB
TypeScript

/**
* #11284 — Antigravity OAuth connect-time DEGRADE marking for accounts without
* a Cloud Code projectId. Shared helper used by the OAuth route's `exchange`,
* `poll-callback`, and the shared persistOAuthConnection path.
*
* Maintainer direction on #11284: do NOT reject the connect — SAVE the
* connection but mark it degraded, so the refresh token stays stored and the
* request-time bootstrap can self-heal it (persistDiscoveredAntigravityProjectId
* flips the row back to active). Confirmed-BYOP accounts get disabled by
* markAntigravityMissingCloudCodeProject() on the first dispatch instead.
*/
export type AntigravityDegradedProjectState = {
/** Persist with this status instead of "active". */
testStatus: "degraded";
errorCode: string;
lastErrorType: string;
lastError: string;
/** Non-fatal warning surfaced in the connect response for the dashboard. */
warning: string;
};
/** Providers whose Cloud Code projectId is expected at connect time. */
const PROJECT_EXPECTED_PROVIDERS = new Set(["antigravity", "agy"]);
const BYOP_WARNING =
"Connected, but Google did not assign a Cloud Code project to this account (BYOP). " +
"Create a GCP Project at console.cloud.google.com and complete Gemini Code Assist onboarding; " +
"the account is marked degraded until then and cannot serve requests.";
const DISCOVERY_FAILED_WARNING =
"Connected, but the Google Cloud Code projectId could not be discovered during login " +
"(loadCodeAssist/onboardUser failed). The account is marked degraded; discovery retries " +
"automatically on the first request.";
function toRecord(value: unknown): Record<string, unknown> {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
}
/** Cloud Code project id from either the mapped token top-level or providerSpecificData. */
function extractAntigravityProjectId(
tokenData: Record<string, unknown> | null | undefined
): string {
if (!tokenData) return "";
const nested = toRecord(tokenData.providerSpecificData);
for (const candidate of [tokenData.projectId, nested.projectId]) {
if (typeof candidate === "string" && candidate.trim()) return candidate.trim();
}
return "";
}
/**
* #11284: when projectId discovery failed at connect time, return the degrade
* fields to persist (testStatus:"degraded" + typed error markers) instead of
* silently saving a false "active". Returns null for healthy payloads.
*
* The original gate only fired when `projectDiscoveryOutcome` was set. Paste
* credentials, persistOAuthConnection, and agy CLI import can persist an empty
* projectId without that flag -- treat the empty id itself as the signal.
*/
export function antigravityDegradedProjectState(
provider: string,
tokenData: Record<string, unknown> | null | undefined
): AntigravityDegradedProjectState | null {
if (!PROJECT_EXPECTED_PROVIDERS.has(provider)) return null;
if (extractAntigravityProjectId(tokenData)) return null;
const outcome = tokenData?.projectDiscoveryOutcome;
const resolvedOutcome =
outcome === "discovery_failed" ? "discovery_failed" : "requires_manual_project";
console.warn(
`[oauth] ${provider}: marking connection degraded - no Cloud Code projectId (${String(resolvedOutcome)}) (#11284)`
);
return {
testStatus: "degraded",
errorCode: "missing_project_id",
lastErrorType: "oauth_missing_project_id",
lastError:
resolvedOutcome === "requires_manual_project" ? BYOP_WARNING : DISCOVERY_FAILED_WARNING,
warning:
resolvedOutcome === "requires_manual_project" ? BYOP_WARNING : DISCOVERY_FAILED_WARNING,
};
}
/** Persist fields that MUST win over a spread tokenData payload. */
export function antigravityPersistStatus(
degradedProject: AntigravityDegradedProjectState | null | undefined
): {
testStatus: "degraded" | "active";
errorCode: string | null;
lastErrorType: string | null;
lastError: string | null;
} {
if (degradedProject) {
return {
testStatus: degradedProject.testStatus,
errorCode: degradedProject.errorCode,
lastErrorType: degradedProject.lastErrorType,
lastError: degradedProject.lastError,
};
}
return {
testStatus: "active",
errorCode: null,
lastErrorType: null,
lastError: null,
};
}