@cortexkit/opencode-antigravity-auth 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +24 -8
  2. package/dist/index.js +1299 -431
  3. package/dist/index.js.map +4 -4
  4. package/dist/src/plugin/accounts.d.ts +29 -9
  5. package/dist/src/plugin/accounts.d.ts.map +1 -1
  6. package/dist/src/plugin/accounts.js +253 -52
  7. package/dist/src/plugin/accounts.js.map +1 -1
  8. package/dist/src/plugin/agy-request-metadata.d.ts +2 -0
  9. package/dist/src/plugin/agy-request-metadata.d.ts.map +1 -0
  10. package/dist/src/plugin/agy-request-metadata.js +2 -0
  11. package/dist/src/plugin/agy-request-metadata.js.map +1 -0
  12. package/dist/src/plugin/auth-doctor.d.ts +1 -1
  13. package/dist/src/plugin/auth-doctor.d.ts.map +1 -1
  14. package/dist/src/plugin/auth-doctor.js +9 -0
  15. package/dist/src/plugin/auth-doctor.js.map +1 -1
  16. package/dist/src/plugin/image-saver.d.ts +2 -21
  17. package/dist/src/plugin/image-saver.d.ts.map +1 -1
  18. package/dist/src/plugin/image-saver.js +29 -63
  19. package/dist/src/plugin/image-saver.js.map +1 -1
  20. package/dist/src/plugin/request-helpers.d.ts +1 -0
  21. package/dist/src/plugin/request-helpers.d.ts.map +1 -1
  22. package/dist/src/plugin/request-helpers.js +9 -4
  23. package/dist/src/plugin/request-helpers.js.map +1 -1
  24. package/dist/src/plugin/request.d.ts +6 -0
  25. package/dist/src/plugin/request.d.ts.map +1 -1
  26. package/dist/src/plugin/request.js +230 -57
  27. package/dist/src/plugin/request.js.map +1 -1
  28. package/dist/src/plugin/session-context.d.ts +21 -0
  29. package/dist/src/plugin/session-context.d.ts.map +1 -0
  30. package/dist/src/plugin/session-context.js +63 -0
  31. package/dist/src/plugin/session-context.js.map +1 -0
  32. package/dist/src/plugin/storage.d.ts +6 -0
  33. package/dist/src/plugin/storage.d.ts.map +1 -1
  34. package/dist/src/plugin/storage.js +14 -3
  35. package/dist/src/plugin/storage.js.map +1 -1
  36. package/dist/src/plugin/ui/auth-menu.d.ts +1 -1
  37. package/dist/src/plugin/ui/auth-menu.d.ts.map +1 -1
  38. package/dist/src/plugin/ui/auth-menu.js +3 -1
  39. package/dist/src/plugin/ui/auth-menu.js.map +1 -1
  40. package/dist/src/plugin/ui/quota-status.d.ts.map +1 -1
  41. package/dist/src/plugin/ui/quota-status.js +2 -1
  42. package/dist/src/plugin/ui/quota-status.js.map +1 -1
  43. package/dist/src/plugin.d.ts +16 -0
  44. package/dist/src/plugin.d.ts.map +1 -1
  45. package/dist/src/plugin.js +261 -101
  46. package/dist/src/plugin.js.map +1 -1
  47. package/package.json +2 -2
@@ -9,9 +9,9 @@ import { fetchWithAgyCliTransport } from "./plugin/agy-transport";
9
9
  import { buildFingerprintHeaders, getSessionFingerprint } from "./plugin/fingerprint";
10
10
  import { clearProvisionFailedKeys, ensureProjectContext } from "./plugin/project";
11
11
  import { startAntigravityDebugRequest, logAntigravityDebugResponse, logAccountContext, logRateLimitEvent, logRateLimitSnapshot, logResponseBody, logModelFamily, isDebugEnabled, getLogFilePath, initializeDebug, } from "./plugin/debug";
12
- import { buildThinkingWarmupBody, getLastCacheStats, isGenerativeLanguageRequest, prepareAntigravityRequest, transformAntigravityResponse, } from "./plugin/request";
12
+ import { buildThinkingWarmupBody, getLastCacheStats, isGenerativeLanguageRequest, getImageModelLocalTitle, prepareAntigravityRequest, transformAntigravityResponse, } from "./plugin/request";
13
13
  import { resolveModelWithTier } from "./plugin/transform/model-resolver";
14
- import { isEmptyResponseBody, createSyntheticErrorResponse, } from "./plugin/request-helpers";
14
+ import { isEmptyResponseBody, createSyntheticErrorResponse, createSyntheticTextResponse, } from "./plugin/request-helpers";
15
15
  import { AntigravityTokenRefreshError, refreshAccessToken } from "./plugin/token";
16
16
  import { startOAuthListener } from "./plugin/server";
17
17
  import { clearAccounts, loadAccounts, saveAccounts, saveAccountsReplace } from "./plugin/storage";
@@ -30,6 +30,8 @@ import { initHealthTracker, getHealthTracker, initTokenTracker, getTokenTracker
30
30
  import { getAntigravityVersionResolution, initAntigravityVersion } from "./plugin/version";
31
31
  import { executeSearch } from "./plugin/search";
32
32
  import { resolvePromptContext } from "./plugin/prompt-context";
33
+ import { AgySessionRegistry, extractOpenCodeSessionIdentity } from "./plugin/session-context";
34
+ import { buildAgyAgentRequestMetadata, createAgyRequestSessionContext, orderAgyRequestPayloadInPlace, } from "./plugin/agy-request-metadata";
33
35
  import { dumpGeminiRequest, executeGeminiDumpCommand, GEMINI_DUMP_COMMAND_NAME, noteGeminiDumpResponse, parseGeminiDumpCommandAction, setGeminiDumpEnabled, } from "./plugin/gemini-dump";
34
36
  import { getAntigravityOpencodeModelIds, OPENCODE_MODEL_DEFINITIONS } from "./plugin/model-registry";
35
37
  const MAX_OAUTH_ACCOUNTS = 10;
@@ -46,10 +48,6 @@ function getCapacityBackoffDelay(consecutiveFailures) {
46
48
  }
47
49
  const warmupAttemptedSessionIds = new Set();
48
50
  const warmupSucceededSessionIds = new Set();
49
- // Track if this plugin instance is running in a child session (subagent, background task)
50
- // Used to filter toasts based on toast_scope config
51
- let isChildSession = false;
52
- let childSessionParentID = undefined;
53
51
  const log = createLogger("plugin");
54
52
  // Module-level toast debounce to persist across requests (fixes toast spam)
55
53
  const rateLimitToastCooldowns = new Map();
@@ -264,10 +262,12 @@ function selectBestVerificationUrl(urls) {
264
262
  });
265
263
  return unique[0];
266
264
  }
267
- function extractVerificationErrorDetails(bodyText) {
265
+ function extractAccountAccessErrorDetails(bodyText) {
268
266
  const decodedBody = decodeEscapedText(bodyText);
269
267
  const lowerBody = decodedBody.toLowerCase();
270
268
  let validationRequired = lowerBody.includes("validation_required");
269
+ const ineligiblePattern = /(^|[^a-z0-9_])account_ineligible([^a-z0-9_]|$)/i;
270
+ let accountIneligible = ineligiblePattern.test(decodedBody);
271
271
  let message;
272
272
  const verificationUrls = new Set();
273
273
  const collectUrlsFromText = (text) => {
@@ -312,6 +312,9 @@ function extractVerificationErrorDetails(bodyText) {
312
312
  if (lowerValue.includes("validation_required")) {
313
313
  validationRequired = true;
314
314
  }
315
+ if (ineligiblePattern.test(normalizedValue)) {
316
+ accountIneligible = true;
317
+ }
315
318
  if (!message &&
316
319
  (lowerKey.includes("message") || lowerKey.includes("detail") || lowerKey.includes("description"))) {
317
320
  message = normalizedValue;
@@ -352,17 +355,68 @@ function extractVerificationErrorDetails(bodyText) {
352
355
  const fallback = decodedBody
353
356
  .split("\n")
354
357
  .map((line) => line.trim())
355
- .find((line) => line && !line.startsWith("data:") && /(verify|validation|required)/i.test(line));
358
+ .find((line) => line && !line.startsWith("data:") && /(verify|validation|required|ineligible)/i.test(line));
356
359
  if (fallback) {
357
360
  message = fallback;
358
361
  }
359
362
  }
360
363
  return {
361
364
  validationRequired,
365
+ accountIneligible,
362
366
  message,
363
367
  verifyUrl: selectBestVerificationUrl([...verificationUrls]),
364
368
  };
365
369
  }
370
+ function buildAccountAccessProbeRequest(projectId) {
371
+ const wireModel = "gemini-3.5-flash-low";
372
+ const request = {
373
+ contents: [{ role: "user", parts: [{ text: "ping" }] }],
374
+ generationConfig: { maxOutputTokens: 1, temperature: 0 },
375
+ };
376
+ const requestMetadata = buildAgyAgentRequestMetadata(createAgyRequestSessionContext(""), request, wireModel);
377
+ request.labels = requestMetadata.labels;
378
+ request.sessionId = requestMetadata.sessionId;
379
+ orderAgyRequestPayloadInPlace(request);
380
+ return {
381
+ project: projectId,
382
+ requestId: requestMetadata.requestId,
383
+ request,
384
+ model: wireModel,
385
+ userAgent: "antigravity",
386
+ requestType: "agent",
387
+ };
388
+ }
389
+ async function interpretAccountAccessProbeResponse(response) {
390
+ if (response.ok) {
391
+ await response.body?.cancel().catch(() => { });
392
+ return { status: "ok", message: "Account verification check passed." };
393
+ }
394
+ let responseBody = "";
395
+ try {
396
+ responseBody = await response.text();
397
+ }
398
+ catch {
399
+ responseBody = "";
400
+ }
401
+ const extracted = extractAccountAccessErrorDetails(responseBody);
402
+ if (response.status === 403 && extracted.accountIneligible) {
403
+ return {
404
+ status: "ineligible",
405
+ message: extracted.message ?? "Google marked this account as ineligible for Antigravity.",
406
+ };
407
+ }
408
+ if (response.status === 403 && extracted.validationRequired) {
409
+ return {
410
+ status: "verification-required",
411
+ message: extracted.message ?? "Google requires additional account verification.",
412
+ verifyUrl: extracted.verifyUrl,
413
+ };
414
+ }
415
+ return {
416
+ status: "error",
417
+ message: extracted.message ?? `Request failed (${response.status} ${response.statusText}).`,
418
+ };
419
+ }
366
420
  async function verifyAccountAccess(account, client, providerId) {
367
421
  const parsed = parseRefreshParts(account.refreshToken);
368
422
  if (!parsed.refreshToken) {
@@ -403,18 +457,7 @@ async function verifyAccountAccess(account, client, providerId) {
403
457
  "Content-Type": "application/json",
404
458
  "Accept-Encoding": "gzip",
405
459
  };
406
- const requestBody = {
407
- project: projectId,
408
- requestId: `agent/${crypto.randomUUID()}/${Date.now()}/${crypto.randomUUID()}/2`,
409
- request: {
410
- model: "gemini-3-flash",
411
- contents: [{ role: "user", parts: [{ text: "ping" }] }],
412
- generationConfig: { maxOutputTokens: 1, temperature: 0 },
413
- },
414
- model: "gemini-3-flash",
415
- userAgent: "antigravity",
416
- requestType: "agent",
417
- };
460
+ const requestBody = buildAccountAccessProbeRequest(projectId);
418
461
  const controller = new AbortController();
419
462
  const timeoutId = setTimeout(() => controller.abort(), 20000);
420
463
  let response;
@@ -434,29 +477,7 @@ async function verifyAccountAccess(account, client, providerId) {
434
477
  finally {
435
478
  clearTimeout(timeoutId);
436
479
  }
437
- let responseBody = "";
438
- try {
439
- responseBody = await response.text();
440
- }
441
- catch {
442
- responseBody = "";
443
- }
444
- if (response.ok) {
445
- return { status: "ok", message: "Account verification check passed." };
446
- }
447
- const extracted = extractVerificationErrorDetails(responseBody);
448
- if (response.status === 403 && extracted.validationRequired) {
449
- return {
450
- status: "blocked",
451
- message: extracted.message ?? "Google requires additional account verification.",
452
- verifyUrl: extracted.verifyUrl,
453
- };
454
- }
455
- const fallbackMessage = extracted.message ?? `Request failed (${response.status} ${response.statusText}).`;
456
- return {
457
- status: "error",
458
- message: fallbackMessage,
459
- };
480
+ return interpretAccountAccessProbeResponse(response);
460
481
  }
461
482
  async function promptAccountIndexForVerification(accounts) {
462
483
  const { createInterface } = await import("node:readline/promises");
@@ -499,12 +520,22 @@ async function promptOpenVerificationUrl() {
499
520
  function markStoredAccountVerificationRequired(account, reason, verifyUrl) {
500
521
  let changed = false;
501
522
  const wasVerificationRequired = account.verificationRequired === true;
523
+ const timestamp = Date.now();
502
524
  if (!wasVerificationRequired) {
503
525
  account.verificationRequired = true;
504
526
  changed = true;
505
527
  }
506
528
  if (!wasVerificationRequired || account.verificationRequiredAt === undefined) {
507
- account.verificationRequiredAt = Date.now();
529
+ account.verificationRequiredAt = timestamp;
530
+ changed = true;
531
+ }
532
+ if (account.accountIneligible === true ||
533
+ account.accountIneligibleAt !== undefined ||
534
+ account.accountIneligibleReason !== undefined) {
535
+ account.accountIneligible = false;
536
+ account.accountIneligibleAt = undefined;
537
+ account.accountIneligibleReason = undefined;
538
+ account.eligibilityStateUpdatedAt = timestamp;
508
539
  changed = true;
509
540
  }
510
541
  const normalizedReason = reason.trim();
@@ -523,8 +554,31 @@ function markStoredAccountVerificationRequired(account, reason, verifyUrl) {
523
554
  }
524
555
  return changed;
525
556
  }
526
- function clearStoredAccountVerificationRequired(account, enableIfRequired = false) {
557
+ function markStoredAccountIneligible(account, reason) {
558
+ const timestamp = Date.now();
559
+ const normalizedReason = reason.trim() || "Google marked this account as ineligible.";
560
+ const changed = account.accountIneligible !== true ||
561
+ account.accountIneligibleReason !== normalizedReason ||
562
+ account.verificationRequired === true ||
563
+ account.verificationRequiredAt !== undefined ||
564
+ account.verificationRequiredReason !== undefined ||
565
+ account.verificationUrl !== undefined ||
566
+ account.enabled !== false;
567
+ account.accountIneligible = true;
568
+ account.accountIneligibleAt = timestamp;
569
+ account.accountIneligibleReason = normalizedReason;
570
+ account.eligibilityStateUpdatedAt = timestamp;
571
+ account.verificationRequired = false;
572
+ account.verificationRequiredAt = undefined;
573
+ account.verificationRequiredReason = undefined;
574
+ account.verificationUrl = undefined;
575
+ account.enabled = false;
576
+ return changed;
577
+ }
578
+ function clearStoredAccountAccessBlocks(account, enableIfBlocked = false) {
527
579
  const wasVerificationRequired = account.verificationRequired === true;
580
+ const wasIneligible = account.accountIneligible === true;
581
+ const wasAccessBlocked = wasVerificationRequired || wasIneligible;
528
582
  let changed = false;
529
583
  if (account.verificationRequired !== false) {
530
584
  account.verificationRequired = false;
@@ -542,11 +596,27 @@ function clearStoredAccountVerificationRequired(account, enableIfRequired = fals
542
596
  account.verificationUrl = undefined;
543
597
  changed = true;
544
598
  }
545
- if (enableIfRequired && wasVerificationRequired && account.enabled === false) {
599
+ if (account.accountIneligible !== false) {
600
+ account.accountIneligible = false;
601
+ changed = true;
602
+ }
603
+ if (account.accountIneligibleAt !== undefined) {
604
+ account.accountIneligibleAt = undefined;
605
+ changed = true;
606
+ }
607
+ if (account.accountIneligibleReason !== undefined) {
608
+ account.accountIneligibleReason = undefined;
609
+ changed = true;
610
+ }
611
+ if (wasIneligible || account.eligibilityStateUpdatedAt !== undefined) {
612
+ account.eligibilityStateUpdatedAt = Date.now();
613
+ changed = true;
614
+ }
615
+ if (enableIfBlocked && wasAccessBlocked && account.enabled === false) {
546
616
  account.enabled = true;
547
617
  changed = true;
548
618
  }
549
- return { changed, wasVerificationRequired };
619
+ return { changed, wasAccessBlocked };
550
620
  }
551
621
  async function promptOAuthCallbackValue(message) {
552
622
  const { createInterface } = await import("node:readline/promises");
@@ -1044,6 +1114,7 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1044
1114
  // Load configuration from files and environment variables
1045
1115
  const config = loadConfig(directory);
1046
1116
  initRuntimeConfig(config);
1117
+ const agySessionRegistry = new AgySessionRegistry(directory);
1047
1118
  // Cached getAuth function for tool access
1048
1119
  let cachedGetAuth = null;
1049
1120
  // Initialize debug with config
@@ -1087,31 +1158,36 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1087
1158
  const eventHandler = async (input) => {
1088
1159
  // Forward to update checker
1089
1160
  await updateChecker.event(input);
1090
- // Track if this is a child session (subagent, background task)
1091
- // This is used to filter toasts based on toast_scope config
1092
1161
  if (input.event.type === "session.created") {
1093
- // Log previous session's quota summary before resetting for new session
1094
- const prevSummary = activeAccountManager?.getSessionSummary();
1095
- if (prevSummary && (prevSummary.totalClaude > 0 || prevSummary.totalGemini > 0)) {
1096
- log.debug("prev-session-quota-summary", {
1097
- durationMinutes: prevSummary.durationMinutes,
1098
- totalClaude: prevSummary.totalClaude,
1099
- totalGemini: prevSummary.totalGemini,
1100
- requestsPerHour: prevSummary.requestsPerHour,
1101
- accountsUsed: prevSummary.accountsUsed,
1102
- });
1103
- }
1104
1162
  const props = input.event.properties;
1105
- if (props?.info?.parentID) {
1106
- isChildSession = true;
1107
- childSessionParentID = props.info.parentID;
1108
- log.debug("child-session-detected", { parentID: props.info.parentID });
1163
+ const sessionId = props?.info?.id;
1164
+ const parentSessionId = props?.info?.parentID ?? null;
1165
+ if (sessionId) {
1166
+ agySessionRegistry.register(sessionId, parentSessionId);
1167
+ }
1168
+ if (parentSessionId) {
1169
+ log.debug("child-session-detected", { sessionId, parentID: parentSessionId });
1109
1170
  }
1110
1171
  else {
1111
- // Reset for root sessions - important when plugin instance is reused
1112
- isChildSession = false;
1113
- childSessionParentID = undefined;
1114
- log.debug("root-session-detected", {});
1172
+ const prevSummary = activeAccountManager?.getSessionSummary();
1173
+ if (prevSummary && (prevSummary.totalClaude > 0 || prevSummary.totalGemini > 0)) {
1174
+ log.debug("prev-session-quota-summary", {
1175
+ durationMinutes: prevSummary.durationMinutes,
1176
+ totalClaude: prevSummary.totalClaude,
1177
+ totalGemini: prevSummary.totalGemini,
1178
+ requestsPerHour: prevSummary.requestsPerHour,
1179
+ accountsUsed: prevSummary.accountsUsed,
1180
+ });
1181
+ }
1182
+ log.debug("root-session-detected", { sessionId });
1183
+ }
1184
+ }
1185
+ if (input.event.type === "session.deleted") {
1186
+ const props = input.event.properties;
1187
+ const sessionId = props?.sessionID ?? props?.info?.id;
1188
+ if (sessionId) {
1189
+ agySessionRegistry.delete(sessionId);
1190
+ activeAccountManager?.deleteSessionState(sessionId);
1115
1191
  }
1116
1192
  }
1117
1193
  // Handle session recovery
@@ -1140,8 +1216,13 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1140
1216
  }).catch(() => { });
1141
1217
  // Show success toast (respects toast_scope for child sessions)
1142
1218
  const successToast = getRecoverySuccessToast();
1143
- log.debug("recovery-toast", { ...successToast, isChildSession, toastScope: config.toast_scope });
1144
- if (!(config.toast_scope === "root_only" && isChildSession)) {
1219
+ const isChildRecovery = agySessionRegistry.getParentSessionId(sessionID) !== null;
1220
+ log.debug("recovery-toast", {
1221
+ ...successToast,
1222
+ isChildSession: isChildRecovery,
1223
+ toastScope: config.toast_scope,
1224
+ });
1225
+ if (!(config.toast_scope === "root_only" && isChildRecovery)) {
1145
1226
  await client.tui.showToast({
1146
1227
  body: {
1147
1228
  title: successToast.title,
@@ -1341,6 +1422,22 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1341
1422
  input = String(input.href ?? input);
1342
1423
  }
1343
1424
  }
1425
+ const localImageTitle = getImageModelLocalTitle(input, init);
1426
+ if (localImageTitle !== undefined) {
1427
+ return createSyntheticTextResponse(localImageTitle, {
1428
+ "X-Antigravity-Response-Type": "local_title",
1429
+ });
1430
+ }
1431
+ const requestSessionIdentity = extractOpenCodeSessionIdentity(init?.headers);
1432
+ const agyRequestScope = agySessionRegistry.beginRequest(requestSessionIdentity);
1433
+ const agyRequestSession = agyRequestScope.session;
1434
+ const accountSessionIdentity = requestSessionIdentity.sessionId
1435
+ ? {
1436
+ id: requestSessionIdentity.sessionId,
1437
+ parentId: requestSessionIdentity.parentSessionId,
1438
+ }
1439
+ : undefined;
1440
+ const isChildRequest = requestSessionIdentity.parentSessionId !== null;
1344
1441
  if (accountManager.getAccountCount() === 0) {
1345
1442
  return createSyntheticErrorResponse("No Antigravity accounts configured. Run `opencode auth login`.", "unknown");
1346
1443
  }
@@ -1354,6 +1451,11 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1354
1451
  debugLines.push(line);
1355
1452
  };
1356
1453
  pushDebug(`request=${urlString}`);
1454
+ if (requestSessionIdentity.sessionId) {
1455
+ pushDebug(`[Session] id=${requestSessionIdentity.sessionId}` +
1456
+ ` parent=${requestSessionIdentity.parentSessionId ?? "none"}` +
1457
+ ` child=${isChildRequest}`);
1458
+ }
1357
1459
  const cachedStats = getLastCacheStats();
1358
1460
  if (cachedStats) {
1359
1461
  const label = cachedStats.hitRate > 0 ? "HIT" : "MISS";
@@ -1375,14 +1477,18 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1375
1477
  // Helper to show toast without blocking on abort (respects quiet_mode and toast_scope)
1376
1478
  const showToast = async (message, variant) => {
1377
1479
  // Always log to debug regardless of toast filtering
1378
- log.debug("toast", { message, variant, isChildSession, toastScope });
1480
+ log.debug("toast", { message, variant, isChildSession: isChildRequest, toastScope });
1379
1481
  if (quietMode)
1380
1482
  return;
1381
1483
  if (abortSignal?.aborted)
1382
1484
  return;
1383
1485
  // Filter toasts for child sessions when toast_scope is "root_only"
1384
- if (toastScope === "root_only" && isChildSession) {
1385
- log.debug("toast-suppressed-child-session", { message, variant, parentID: childSessionParentID });
1486
+ if (toastScope === "root_only" && isChildRequest) {
1487
+ log.debug("toast-suppressed-child-session", {
1488
+ message,
1489
+ variant,
1490
+ parentID: requestSessionIdentity.parentSessionId,
1491
+ });
1386
1492
  return;
1387
1493
  }
1388
1494
  if (variant === "warning" && message.toLowerCase().includes("rate")) {
@@ -1419,10 +1525,10 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1419
1525
  return createSyntheticErrorResponse("No Antigravity accounts available. Run `opencode auth login`.", model ?? "unknown");
1420
1526
  }
1421
1527
  const softQuotaCacheTtlMs = computeSoftQuotaCacheTtlMs(config.soft_quota_cache_ttl_minutes, config.quota_refresh_interval_minutes);
1422
- let account = accountManager.getCurrentOrNextForFamily(family, model, config.account_selection_strategy, preferredHeaderStyle, config.pid_offset_enabled, config.soft_quota_threshold_percent, softQuotaCacheTtlMs);
1528
+ let account = accountManager.getCurrentOrNextForFamily(family, model, config.account_selection_strategy, preferredHeaderStyle, config.pid_offset_enabled, config.soft_quota_threshold_percent, softQuotaCacheTtlMs, accountSessionIdentity);
1423
1529
  if (!account && allowQuotaFallback) {
1424
1530
  const alternateHeaderStyle = preferredHeaderStyle === "antigravity" ? "gemini-cli" : "antigravity";
1425
- account = accountManager.getCurrentOrNextForFamily(family, model, config.account_selection_strategy, alternateHeaderStyle, config.pid_offset_enabled, config.soft_quota_threshold_percent, softQuotaCacheTtlMs);
1531
+ account = accountManager.getCurrentOrNextForFamily(family, model, config.account_selection_strategy, alternateHeaderStyle, config.pid_offset_enabled, config.soft_quota_threshold_percent, softQuotaCacheTtlMs, accountSessionIdentity);
1426
1532
  if (account) {
1427
1533
  pushDebug(`selected-by-fallback idx=${account.index} preferred=${preferredHeaderStyle} alternate=${alternateHeaderStyle}`);
1428
1534
  }
@@ -1488,7 +1594,7 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1488
1594
  pushDebug(`account-switch: ${previousAccountIndex} → ${account.index}, warmup=${needsCacheWarmup}`);
1489
1595
  }
1490
1596
  previousAccountIndex = account.index;
1491
- accountManager.recordSessionUsage(account.index);
1597
+ accountManager.recordSessionUsage(account.index, accountSessionIdentity);
1492
1598
  if (isDebugEnabled()) {
1493
1599
  logAccountContext("Selected", {
1494
1600
  index: account.index,
@@ -1796,6 +1902,8 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1796
1902
  claudeToolHardening: config.claude_tool_hardening,
1797
1903
  claudePromptAutoCaching: config.claude_prompt_auto_caching,
1798
1904
  fingerprint: account.fingerprint,
1905
+ agySession: agyRequestSession,
1906
+ agyRequestTimestamp: agyRequestScope.timestamp,
1799
1907
  });
1800
1908
  const originalUrl = toUrlString(input);
1801
1909
  const resolvedUrl = toUrlString(prepared.request);
@@ -2041,7 +2149,23 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
2041
2149
  resetAccountFailureState(account.index);
2042
2150
  if (response.status === 403) {
2043
2151
  const errorBodyText = await response.clone().text().catch(() => "");
2044
- const extracted = extractVerificationErrorDetails(errorBodyText);
2152
+ const extracted = extractAccountAccessErrorDetails(errorBodyText);
2153
+ if (extracted.accountIneligible) {
2154
+ const ineligibleReason = extracted.message ??
2155
+ "Google marked this account as ineligible for Antigravity.";
2156
+ accountManager.markAccountIneligible(account.index, ineligibleReason);
2157
+ const label = account.email || `Account ${account.index + 1}`;
2158
+ if (accountManager.shouldShowAccountToast(account.index, 60000)) {
2159
+ await showToast(`${label} is not eligible for Antigravity and has been disabled. ` +
2160
+ "Recheck it from opencode auth login > Verify accounts.", "warning");
2161
+ accountManager.markToastShown(account.index);
2162
+ }
2163
+ pushDebug(`account-ineligible: disabled account ${account.index}`);
2164
+ getHealthTracker().recordFailure(account.index);
2165
+ lastFailure = createFailureContext(response);
2166
+ shouldSwitchAccount = true;
2167
+ break;
2168
+ }
2045
2169
  if (extracted.validationRequired) {
2046
2170
  const verificationReason = extracted.message ?? "Google requires account verification.";
2047
2171
  const cooldownMs = 10 * 60 * 1000;
@@ -2077,13 +2201,14 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
2077
2201
  // Proactive rotation: if current account quota is low, pre-switch
2078
2202
  // to a warm-cache account so the NEXT request avoids a cold cache miss
2079
2203
  const proactiveThreshold = config.proactive_rotation_threshold_percent ?? 20;
2080
- if (proactiveThreshold > 0 && accountManager.shouldProactivelyRotate(family, model, proactiveThreshold, softQuotaCacheTtlMs)) {
2081
- const rotated = accountManager.proactivelyRotateForFamily(family, model, headerStyle, config.soft_quota_threshold_percent, softQuotaCacheTtlMs);
2204
+ if (proactiveThreshold > 0 && accountManager.shouldProactivelyRotate(family, model, proactiveThreshold, softQuotaCacheTtlMs, accountSessionIdentity)) {
2205
+ const rotated = accountManager.proactivelyRotateForFamily(family, model, headerStyle, config.soft_quota_threshold_percent, softQuotaCacheTtlMs, accountSessionIdentity);
2082
2206
  if (rotated) {
2083
2207
  const remaining = account.cachedQuota?.[resolveQuotaGroup(family, model)]?.remainingFraction;
2084
2208
  const remainingPct = remaining != null ? `${(remaining * 100).toFixed(1)}%` : "?";
2085
2209
  pushDebug(`[ProactiveRotation] account ${account.index} quota ${remainingPct} < ${proactiveThreshold}%, pre-switched to account ${rotated.index} for next request`);
2086
- pushDebug(`[ProactiveRotation] ${account.index} → ${rotated.index} (warm=${accountManager.wasUsedInSession(rotated.index)})`);
2210
+ pushDebug(`[ProactiveRotation] ${account.index} → ${rotated.index}` +
2211
+ ` (warm=${accountManager.wasUsedInSession(rotated.index, accountSessionIdentity)})`);
2087
2212
  }
2088
2213
  }
2089
2214
  }
@@ -2157,7 +2282,7 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
2157
2282
  // Post-request quota state: show cached remaining quota for this account
2158
2283
  const cachedQuota = account.cachedQuota;
2159
2284
  if (cachedQuota) {
2160
- const quotaFamily = family === "claude" ? "claude" : "gemini-flash";
2285
+ const quotaFamily = resolveQuotaGroup(family, model);
2161
2286
  const groupQuota = cachedQuota[quotaFamily];
2162
2287
  if (groupQuota?.remainingFraction != null) {
2163
2288
  const pct = Math.round(groupQuota.remainingFraction * 100);
@@ -2273,26 +2398,29 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
2273
2398
  while (true) {
2274
2399
  const now = Date.now();
2275
2400
  const existingAccounts = existingStorage.accounts.map((acc, idx) => {
2276
- let status = 'unknown';
2277
- if (acc.verificationRequired) {
2278
- status = 'verification-required';
2401
+ let status = "unknown";
2402
+ if (acc.accountIneligible) {
2403
+ status = "ineligible";
2404
+ }
2405
+ else if (acc.verificationRequired) {
2406
+ status = "verification-required";
2279
2407
  }
2280
2408
  else {
2281
2409
  const rateLimits = acc.rateLimitResetTimes;
2282
2410
  if (rateLimits) {
2283
- const isRateLimited = Object.values(rateLimits).some((resetTime) => typeof resetTime === 'number' && resetTime > now);
2411
+ const isRateLimited = Object.values(rateLimits).some((resetTime) => typeof resetTime === "number" && resetTime > now);
2284
2412
  if (isRateLimited) {
2285
- status = 'rate-limited';
2413
+ status = "rate-limited";
2286
2414
  }
2287
2415
  else {
2288
- status = 'active';
2416
+ status = "active";
2289
2417
  }
2290
2418
  }
2291
2419
  else {
2292
- status = 'active';
2420
+ status = "active";
2293
2421
  }
2294
2422
  if (acc.coolingDownUntil && acc.coolingDownUntil > now) {
2295
- status = 'rate-limited';
2423
+ status = "rate-limited";
2296
2424
  }
2297
2425
  }
2298
2426
  const cooldownMs = (acc.coolingDownUntil && acc.coolingDownUntil > now)
@@ -2306,11 +2434,11 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
2306
2434
  || (now - acc.cachedQuotaUpdatedAt) > DISPLAY_QUOTA_MAX_AGE_MS;
2307
2435
  const displayQuota = quotaIsStale ? undefined : acc.cachedQuota;
2308
2436
  const displayPerModelQuota = quotaIsStale ? undefined : acc.cachedPerModelQuota;
2309
- if (status === 'active' && displayQuota) {
2437
+ if (status === "active" && displayQuota) {
2310
2438
  const groups = Object.values(displayQuota);
2311
2439
  const allExhausted = groups.length > 0 && groups.every((group) => typeof group.remainingFraction === "number" && group.remainingFraction <= 0);
2312
2440
  if (allExhausted) {
2313
- status = 'rate-limited';
2441
+ status = "rate-limited";
2314
2442
  }
2315
2443
  }
2316
2444
  return {
@@ -2430,6 +2558,7 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
2430
2558
  { name: "Claude", data: groups.claude },
2431
2559
  { name: "Gemini 3 Pro", data: groups["gemini-pro"] },
2432
2560
  { name: "Gemini 3 Flash", data: groups["gemini-flash"] },
2561
+ { name: "GPT-OSS", data: groups["gpt-oss"] },
2433
2562
  ].filter(g => g.data);
2434
2563
  groupEntries.forEach((g, idx) => {
2435
2564
  const isLast = idx === groupEntries.length - 1;
@@ -2487,10 +2616,16 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
2487
2616
  if (menuResult.toggleAccountIndex !== undefined) {
2488
2617
  const acc = existingStorage.accounts[menuResult.toggleAccountIndex];
2489
2618
  if (acc) {
2490
- acc.enabled = acc.enabled === false;
2619
+ const shouldEnable = acc.enabled === false;
2620
+ if (shouldEnable && acc.accountIneligible) {
2621
+ console.log(`\n${acc.email || `Account ${menuResult.toggleAccountIndex + 1}`} remains disabled. ` +
2622
+ "Use Verify accounts to recheck eligibility.\n");
2623
+ continue;
2624
+ }
2625
+ acc.enabled = shouldEnable;
2491
2626
  await saveAccounts(existingStorage);
2492
2627
  activeAccountManager?.setAccountEnabled(menuResult.toggleAccountIndex, acc.enabled);
2493
- console.log(`\nAccount ${acc.email || menuResult.toggleAccountIndex + 1} ${acc.enabled ? 'enabled' : 'disabled'}.\n`);
2628
+ console.log(`\nAccount ${acc.email || menuResult.toggleAccountIndex + 1} ${acc.enabled ? "enabled" : "disabled"}.\n`);
2494
2629
  }
2495
2630
  }
2496
2631
  continue;
@@ -2505,6 +2640,7 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
2505
2640
  console.log(`\nChecking verification status for ${existingStorage.accounts.length} account(s)...\n`);
2506
2641
  let okCount = 0;
2507
2642
  let blockedCount = 0;
2643
+ let ineligibleCount = 0;
2508
2644
  let errorCount = 0;
2509
2645
  let storageUpdated = false;
2510
2646
  const blockedResults = [];
@@ -2516,16 +2652,16 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
2516
2652
  process.stdout.write(`- [${i + 1}/${existingStorage.accounts.length}] ${label} ... `);
2517
2653
  const verification = await verifyAccountAccess(account, client, providerId);
2518
2654
  if (verification.status === "ok") {
2519
- const { changed, wasVerificationRequired } = clearStoredAccountVerificationRequired(account, true);
2655
+ const { changed, wasAccessBlocked } = clearStoredAccountAccessBlocks(account, true);
2520
2656
  if (changed) {
2521
2657
  storageUpdated = true;
2522
2658
  }
2523
- activeAccountManager?.clearAccountVerificationRequired(i, wasVerificationRequired);
2659
+ activeAccountManager?.clearAccountAccessBlocks(i, wasAccessBlocked);
2524
2660
  okCount += 1;
2525
2661
  console.log("ok");
2526
2662
  continue;
2527
2663
  }
2528
- if (verification.status === "blocked") {
2664
+ if (verification.status === "verification-required") {
2529
2665
  const changed = markStoredAccountVerificationRequired(account, verification.message, verification.verifyUrl);
2530
2666
  if (changed) {
2531
2667
  storageUpdated = true;
@@ -2541,13 +2677,24 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
2541
2677
  });
2542
2678
  continue;
2543
2679
  }
2680
+ if (verification.status === "ineligible") {
2681
+ const changed = markStoredAccountIneligible(account, verification.message);
2682
+ if (changed) {
2683
+ storageUpdated = true;
2684
+ }
2685
+ activeAccountManager?.markAccountIneligible(i, verification.message);
2686
+ ineligibleCount += 1;
2687
+ console.log("ineligible");
2688
+ continue;
2689
+ }
2544
2690
  errorCount += 1;
2545
2691
  console.log(`error (${verification.message})`);
2546
2692
  }
2547
2693
  if (storageUpdated) {
2548
2694
  await saveAccounts(existingStorage);
2549
2695
  }
2550
- console.log(`\nVerification summary: ${okCount} ready, ${blockedCount} need verification, ${errorCount} errors.`);
2696
+ console.log(`\nVerification summary: ${okCount} ready, ${blockedCount} need verification, ` +
2697
+ `${ineligibleCount} ineligible, ${errorCount} errors.`);
2551
2698
  if (blockedResults.length > 0) {
2552
2699
  console.log("\nAccounts needing verification:");
2553
2700
  for (const result of blockedResults) {
@@ -2584,12 +2731,12 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
2584
2731
  console.log(`\nChecking verification status for ${label}...\n`);
2585
2732
  const verification = await verifyAccountAccess(account, client, providerId);
2586
2733
  if (verification.status === "ok") {
2587
- const { changed, wasVerificationRequired } = clearStoredAccountVerificationRequired(account, true);
2734
+ const { changed, wasAccessBlocked } = clearStoredAccountAccessBlocks(account, true);
2588
2735
  if (changed) {
2589
2736
  await saveAccounts(existingStorage);
2590
2737
  }
2591
- activeAccountManager?.clearAccountVerificationRequired(verifyAccountIndex, wasVerificationRequired);
2592
- if (wasVerificationRequired) {
2738
+ activeAccountManager?.clearAccountAccessBlocks(verifyAccountIndex, wasAccessBlocked);
2739
+ if (wasAccessBlocked) {
2593
2740
  console.log(`✓ ${label} is ready for requests and has been re-enabled.\n`);
2594
2741
  }
2595
2742
  else {
@@ -2597,7 +2744,7 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
2597
2744
  }
2598
2745
  continue;
2599
2746
  }
2600
- if (verification.status === "blocked") {
2747
+ if (verification.status === "verification-required") {
2601
2748
  const changed = markStoredAccountVerificationRequired(account, verification.message, verification.verifyUrl);
2602
2749
  if (changed) {
2603
2750
  await saveAccounts(existingStorage);
@@ -2626,6 +2773,16 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
2626
2773
  }
2627
2774
  continue;
2628
2775
  }
2776
+ if (verification.status === "ineligible") {
2777
+ const changed = markStoredAccountIneligible(account, verification.message);
2778
+ if (changed) {
2779
+ await saveAccounts(existingStorage);
2780
+ }
2781
+ activeAccountManager?.markAccountIneligible(verifyAccountIndex, verification.message);
2782
+ console.log(`⚠ ${label} is not eligible for Antigravity and has been disabled.`);
2783
+ console.log(`${verification.message}\n`);
2784
+ continue;
2785
+ }
2629
2786
  console.log(`✗ ${label}: ${verification.message}\n`);
2630
2787
  continue;
2631
2788
  }
@@ -3143,6 +3300,9 @@ function isExplicitQuotaFromUrl(urlString) {
3143
3300
  return explicitQuota ?? false;
3144
3301
  }
3145
3302
  export const __testExports = {
3303
+ buildAccountAccessProbeRequest,
3304
+ extractAccountAccessErrorDetails,
3305
+ interpretAccountAccessProbeResponse,
3146
3306
  getHeaderStyleFromUrl,
3147
3307
  isCapacityRetryBudgetExhausted,
3148
3308
  resolveHeaderRoutingDecision,