mixdog 0.9.166 → 0.9.167

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 (221) hide show
  1. package/package.json +1 -1
  2. package/scripts/audit-usage-ledger.mjs +86 -0
  3. package/scripts/generate-voice-runtime-manifest.mjs +22 -17
  4. package/scripts/memory-chunk-quality.mjs +128 -0
  5. package/scripts/memory-layer2-check.mjs +133 -0
  6. package/scripts/model-catalog-audit.mjs +6 -0
  7. package/scripts/native-tool-download.mjs +14 -0
  8. package/scripts/prepare-native-assets.mjs +2 -7
  9. package/scripts/tool-failures.mjs +7 -2
  10. package/scripts/verify-release-assets.mjs +48 -90
  11. package/src/defaults/skills/browser-use/SKILL.md +33 -18
  12. package/src/defaults/skills/computer-use/SKILL.md +16 -8
  13. package/src/lib/keychain-cjs.cjs +0 -1
  14. package/src/lib/rules-builder.cjs +4 -1
  15. package/src/rules/agent/40-cycle1-agent.md +21 -13
  16. package/src/rules/lead/01-general.md +8 -3
  17. package/src/rules/shared/10-tool-workflow.md +20 -11
  18. package/src/rules/shared/30-exploration.md +11 -4
  19. package/src/rules/shared/40-editing.md +2 -3
  20. package/src/rules/shared/50-execution.md +6 -3
  21. package/src/rules/shared/60-verification.md +3 -2
  22. package/src/rules/shared/70-delivery.md +0 -4
  23. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +51 -45
  24. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +3 -4
  25. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +44 -7
  26. package/src/runtime/agent/orchestrator/agent-trace.mjs +9 -0
  27. package/src/runtime/agent/orchestrator/config.mjs +9 -4
  28. package/src/runtime/agent/orchestrator/context/collect.mjs +25 -1055
  29. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +272 -0
  30. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +296 -0
  31. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +523 -0
  32. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +7 -2
  33. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +13 -14
  34. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +214 -0
  35. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +220 -116
  36. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +74 -44
  37. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +191 -29
  38. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +13 -12
  39. package/src/runtime/agent/orchestrator/providers/cursor-wire-guards.mjs +1 -9
  40. package/src/runtime/agent/orchestrator/providers/cursor-wire-protobuf.mjs +5 -2
  41. package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +48 -25
  42. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +28 -2
  43. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +3 -1
  44. package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +1 -8
  45. package/src/runtime/agent/orchestrator/providers/lib/action-input-contract.mjs +32 -0
  46. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +21 -6
  47. package/src/runtime/agent/orchestrator/providers/lib/grok-tool-schema.mjs +61 -13
  48. package/src/runtime/agent/orchestrator/providers/model-catalog-projection.mjs +6 -1
  49. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +123 -118
  50. package/src/runtime/agent/orchestrator/providers/model-pricing-rates.mjs +54 -0
  51. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +5 -2
  52. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +34 -3
  53. package/src/runtime/agent/orchestrator/providers/openai-codex-endpoints.mjs +21 -0
  54. package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +154 -0
  55. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +2 -1
  56. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +322 -0
  57. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +243 -702
  58. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +2 -2
  59. package/src/runtime/agent/orchestrator/providers/openai-startup-prewarm.mjs +221 -0
  60. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -2
  61. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +1 -1
  62. package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +19 -0
  63. package/src/runtime/agent/orchestrator/providers/provider-model-identities.mjs +40 -0
  64. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +120 -710
  65. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +36 -15
  66. package/src/runtime/agent/orchestrator/session/cache/util.mjs +9 -0
  67. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +17 -4
  68. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +36 -31
  69. package/src/runtime/agent/orchestrator/session/compact.mjs +4 -5
  70. package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +6 -5
  71. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +8 -6
  72. package/src/runtime/agent/orchestrator/session/loop/assistant-commit.mjs +121 -0
  73. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +2 -3
  74. package/src/runtime/agent/orchestrator/session/loop/diagnostics.mjs +74 -0
  75. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +44 -17
  76. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +266 -0
  77. package/src/runtime/agent/orchestrator/session/loop/request-boundary.mjs +111 -0
  78. package/src/runtime/agent/orchestrator/session/loop/request-projection.mjs +99 -0
  79. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +91 -0
  80. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +36 -0
  81. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +3 -3
  82. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +12 -0
  83. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +20 -11
  84. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +3 -5
  85. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +9 -3
  86. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +39 -0
  87. package/src/runtime/agent/orchestrator/session/store-transcript-cache.mjs +3 -2
  88. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +21 -3
  89. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +1 -1
  90. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +2 -3
  91. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +25 -25
  92. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +62 -10
  93. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +17 -0
  94. package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +61 -4
  95. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +13 -9
  96. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +12 -2
  97. package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +3 -2
  98. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +8 -3
  99. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +69 -6
  100. package/src/runtime/agent/orchestrator/tools/builtin.mjs +14 -17
  101. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +1 -1
  102. package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +1 -1
  103. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
  104. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +1 -1
  105. package/src/runtime/bridge-discovery.mjs +8 -0
  106. package/src/runtime/browser-bridge/action-schema.mjs +37 -43
  107. package/src/runtime/browser-bridge/client.mjs +111 -99
  108. package/src/runtime/browser-bridge/input-fields.mjs +187 -0
  109. package/src/runtime/browser-bridge/tool-defs.mjs +12 -227
  110. package/src/runtime/computer-bridge/action-schema.mjs +2 -46
  111. package/src/runtime/computer-bridge/client.mjs +69 -77
  112. package/src/runtime/computer-bridge/core-actions.mjs +7 -5
  113. package/src/runtime/computer-bridge/error-recovery.mjs +41 -37
  114. package/src/runtime/computer-bridge/tool-defs.mjs +1 -1
  115. package/src/runtime/github/commands.mjs +4 -1
  116. package/src/runtime/github/contract.mjs +1 -1
  117. package/src/runtime/github/tool.mjs +2 -2
  118. package/src/runtime/media/adapters/antigravity-image.mjs +72 -0
  119. package/src/runtime/media/adapters/gemini-image.mjs +20 -11
  120. package/src/runtime/media/auth.mjs +12 -0
  121. package/src/runtime/media/catalog.mjs +30 -10
  122. package/src/runtime/media/jobs.mjs +4 -0
  123. package/src/runtime/media/lanes.mjs +40 -31
  124. package/src/runtime/memory/index.mjs +2 -9
  125. package/src/runtime/memory/lib/compact-handoff.mjs +26 -18
  126. package/src/runtime/memory/lib/memory-chunk-audit.mjs +41 -0
  127. package/src/runtime/memory/lib/memory-chunk-quality.mjs +388 -0
  128. package/src/runtime/memory/lib/memory-cycle1.mjs +95 -394
  129. package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +29 -4
  130. package/src/runtime/memory/lib/memory-recall-store.mjs +8 -12
  131. package/src/runtime/memory/lib/memory-retrievers.mjs +8 -11
  132. package/src/runtime/memory/lib/memory.mjs +2 -0
  133. package/src/runtime/memory/lib/query-handlers.mjs +98 -144
  134. package/src/runtime/memory/lib/query-ranking.mjs +23 -9
  135. package/src/runtime/memory/lib/recall-format.mjs +37 -27
  136. package/src/runtime/memory/lib/session-ingest-runtime.mjs +14 -8
  137. package/src/runtime/memory/lib/session-ingest.mjs +45 -84
  138. package/src/runtime/memory/lib/transcript-ingest.mjs +19 -21
  139. package/src/runtime/memory/tool-defs.mjs +2 -2
  140. package/src/runtime/office/authoring/pptx-author-session.mjs +3 -7
  141. package/src/runtime/office/core/office-actions-batch.mjs +2 -8
  142. package/src/runtime/office/core/office-actions-open.mjs +124 -0
  143. package/src/runtime/office/core/office-actions-read.mjs +127 -0
  144. package/src/runtime/office/core/office-core.mjs +80 -1
  145. package/src/runtime/office/core/office-sessions.mjs +116 -138
  146. package/src/runtime/office/index.mjs +18 -271
  147. package/src/runtime/office/pdf/pdf-analysis.mjs +2 -2
  148. package/src/runtime/office/pdf/pdf-limits.mjs +2 -0
  149. package/src/runtime/office/tool-defs.mjs +7 -6
  150. package/src/runtime/shared/agent-route-config.mjs +8 -8
  151. package/src/runtime/shared/child-spawn-gate.mjs +10 -13
  152. package/src/runtime/shared/config.mjs +2 -7
  153. package/src/runtime/shared/env.mjs +3 -3
  154. package/src/runtime/shared/err-text.mjs +1 -1
  155. package/src/runtime/shared/extension-scopes.mjs +3 -2
  156. package/src/runtime/shared/heap-cap.mjs +10 -6
  157. package/src/runtime/shared/llm/cost.mjs +35 -10
  158. package/src/runtime/shared/llm/usage-accounting.mjs +11 -1
  159. package/src/runtime/shared/llm/usage-context.mjs +6 -0
  160. package/src/runtime/shared/llm/usage-ledger-import.mjs +5 -0
  161. package/src/runtime/shared/llm/usage-ledger-repair.mjs +109 -0
  162. package/src/runtime/shared/llm/usage-ledger.mjs +59 -20
  163. package/src/runtime/shared/llm/usage-pricing-refresh.mjs +26 -0
  164. package/src/runtime/shared/llm/usage-rollup.mjs +17 -15
  165. package/src/runtime/shared/native-asset.mjs +3 -4
  166. package/src/runtime/shared/object.mjs +4 -0
  167. package/src/runtime/shared/pg-schema-db.mjs +21 -0
  168. package/src/runtime/shared/plugin-metadata.mjs +3 -1
  169. package/src/runtime/shared/resource-admission.mjs +1 -6
  170. package/src/runtime/shared/schedules-db.mjs +8 -24
  171. package/src/runtime/shared/schema-value-error.d.mts +2 -0
  172. package/src/runtime/shared/schema-value-error.mjs +68 -0
  173. package/src/runtime/shared/singleton-owner.mjs +11 -24
  174. package/src/runtime/shared/staged-update.mjs +1 -10
  175. package/src/runtime/shared/tool-card-model.mjs +8 -5
  176. package/src/runtime/shared/tool-execution-contract.mjs +1 -1
  177. package/src/runtime/shared/tool-surface.mjs +1 -5
  178. package/src/runtime/shared/user-data-guard.mjs +1 -4
  179. package/src/runtime/shared/webhooks-db.mjs +8 -24
  180. package/src/runtime/web-search/index.mjs +32 -8
  181. package/src/runtime/web-search/tool-defs.mjs +4 -4
  182. package/src/session-runtime/config-lifecycle.mjs +34 -0
  183. package/src/session-runtime/context-status.mjs +19 -0
  184. package/src/session-runtime/cwd-plugins.mjs +12 -29
  185. package/src/session-runtime/goal-tasks.mjs +2 -2
  186. package/src/session-runtime/goal-tool-defs.mjs +3 -2
  187. package/src/session-runtime/internal-tool-executor.mjs +7 -0
  188. package/src/session-runtime/lifecycle-api.mjs +5 -11
  189. package/src/session-runtime/notification-bus.mjs +41 -0
  190. package/src/session-runtime/prewarm.mjs +22 -0
  191. package/src/session-runtime/runtime-bootstrap.mjs +90 -0
  192. package/src/session-runtime/runtime-core.mjs +88 -202
  193. package/src/session-runtime/runtime-feature-gates.mjs +37 -0
  194. package/src/session-runtime/session-lifecycle.mjs +5 -5
  195. package/src/session-runtime/session-turn-api.mjs +1 -12
  196. package/src/session-runtime/setup-tool/executor.mjs +13 -5
  197. package/src/session-runtime/setup-tool/tool-defs.mjs +52 -46
  198. package/src/session-runtime/tool-defs.mjs +73 -4
  199. package/src/session-runtime/usage-stats-api.mjs +4 -1
  200. package/src/session-runtime/warmup-schedulers.mjs +8 -2
  201. package/src/session-runtime/workflow.mjs +14 -0
  202. package/src/standalone/channel-session-router.mjs +3 -1
  203. package/src/standalone/channel-worker.mjs +18 -9
  204. package/src/standalone/daemon-crash-capture.mjs +534 -0
  205. package/src/standalone/daemon-telemetry.mjs +175 -0
  206. package/src/standalone/daemon.mjs +31 -21
  207. package/src/standalone/session-client.mjs +31 -23
  208. package/src/standalone/usage-dashboard-model.mjs +2 -0
  209. package/src/standalone/usage-stats-hours.mjs +18 -11
  210. package/src/standalone/usage-stats-model.mjs +39 -28
  211. package/src/standalone/usage-stats-period.mjs +43 -45
  212. package/src/tui/app/use-transcript-window.mjs +1 -1
  213. package/src/tui/components/StatusLine.jsx +13 -42
  214. package/src/tui/dist/index.mjs +256 -239
  215. package/src/tui/session/session-api-ext.mjs +44 -41
  216. package/src/tui/session/session-api.mjs +4 -0
  217. package/src/tui/session/tool-card-results.mjs +25 -78
  218. package/src/tui/session/tool-result-status.mjs +94 -1
  219. package/src/tui/session/turn-aggregate-cards.mjs +17 -54
  220. package/src/ui/statusline-format.mjs +6 -26
  221. package/src/workflows/default/WORKFLOW.md +5 -0
@@ -3,12 +3,13 @@
3
3
  *
4
4
  * Flow: PKCE authorization on a loopback callback (port 51121, the port the
5
5
  * registered client is bound to) -> token exchange -> account email ->
6
- * `loadCodeAssist` for an existing Cloud project, falling back to `onboardUser`
7
- * which provisions one. A login without a project id is useless: every content
8
- * request carries `project`.
6
+ * `loadCodeAssist` for account status -> free-tier onboarding when needed ->
7
+ * refreshed Cloud project. A login without a project id is useless: every
8
+ * content request carries `project`.
9
9
  */
10
10
  import { createServer } from 'http';
11
11
  import { randomBytes } from 'crypto';
12
+ import { setTimeout as delay } from 'node:timers/promises';
12
13
  import { createOAuthPkce, parseOAuthCodeInput } from './lib/oauth-pkce.mjs';
13
14
  import {
14
15
  AUTH_URL,
@@ -18,7 +19,7 @@ import {
18
19
  CLIENT_ID,
19
20
  CLIENT_SECRET,
20
21
  LOGIN_TIMEOUT_MS,
21
- PROJECT_ENDPOINTS,
22
+ PROJECT_ENDPOINT,
22
23
  PROJECT_TIMEOUT_MS,
23
24
  REDIRECT_URI,
24
25
  SCOPES,
@@ -31,128 +32,182 @@ import {
31
32
  saveTokens,
32
33
  } from './antigravity-oauth-tokens.mjs';
33
34
 
34
- const ONBOARD_MAX_ATTEMPTS = 8;
35
- const ONBOARD_INTERVAL_MS = 2_000;
35
+ const ONBOARD_INTERVAL_MS = 1_000;
36
+ const CALLBACK_FAILURE_HTML = '<html><body><h2>Antigravity sign-in was not completed.</h2>'
37
+ + '<p>Return to Mixdog for the error details and any account verification link.</p></body></html>';
36
38
 
37
39
  export function generatePKCE() {
38
40
  return createOAuthPkce();
39
41
  }
40
42
 
41
- function readProjectId(value) {
42
- if (typeof value === 'string' && value.trim()) return value.trim();
43
- if (value && typeof value === 'object' && typeof value.id === 'string' && value.id.trim()) {
44
- return value.id.trim();
45
- }
46
- return '';
47
- }
48
-
49
43
  function extractProjectId(payload) {
50
- if (!payload || typeof payload !== 'object') return '';
51
- for (const key of ['cloudaicompanionProject', 'projectId', 'project']) {
52
- const id = readProjectId(payload[key]);
53
- if (id) return id;
54
- }
55
- return '';
44
+ return typeof payload?.cloudaicompanionProject === 'string' ? payload.cloudaicompanionProject : '';
56
45
  }
57
46
 
58
- function defaultTierId(allowedTiers, currentTier) {
59
- if (Array.isArray(allowedTiers) && allowedTiers.length) {
60
- const preferred = allowedTiers.find((tier) => tier?.isDefault && String(tier?.id || '').trim());
61
- if (preferred) return String(preferred.id).trim();
62
- const first = allowedTiers.find((tier) => String(tier?.id || '').trim());
63
- if (first) return String(first.id).trim();
64
- }
65
- const current = String(currentTier?.id || '').trim();
66
- return current || 'free-tier';
47
+ function accountVerificationError(validationUrl, reason, email) {
48
+ const account = email ? ` for ${email}` : '';
49
+ const detail = reason ? `\n${reason}` : '';
50
+ const error = new Error(_scrubTokens(`[antigravity-oauth] Account verification required${account}.${detail}`
51
+ + `\nVisit ${validationUrl} to continue, then sign in again.`));
52
+ error.code = 'VALIDATION_REQUIRED';
53
+ error.validationUrl = validationUrl;
54
+ return error;
67
55
  }
68
56
 
69
- async function postJson(url, body, accessToken, { fetchFn = fetch, signal = null } = {}) {
70
- return await fetchFn(url, {
71
- method: 'POST',
57
+ async function requestCodeAssist(action, body, context, timeoutMs = PROJECT_TIMEOUT_MS) {
58
+ const { accessToken, fetchFn, signal } = context;
59
+ signal?.throwIfAborted();
60
+ const timeout = AbortSignal.timeout(timeoutMs);
61
+ const isPost = body !== undefined;
62
+ const res = await fetchFn(`${PROJECT_ENDPOINT}/v1internal${isPost ? ':' : '/'}${action}`, {
63
+ method: isPost ? 'POST' : 'GET',
72
64
  headers: {
73
65
  'Content-Type': 'application/json',
74
66
  Authorization: `Bearer ${accessToken}`,
75
67
  ...antigravityHeaders(),
76
68
  },
77
- body: JSON.stringify(body),
78
- signal: signal || AbortSignal.timeout(PROJECT_TIMEOUT_MS),
69
+ ...(isPost ? { body: JSON.stringify(body) } : {}),
70
+ redirect: 'error',
71
+ signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
79
72
  });
73
+ if (res.status !== 200) {
74
+ const text = await res.text();
75
+ let failure = null;
76
+ try { failure = JSON.parse(text); } catch { /* Non-JSON errors retain their original detail below. */ }
77
+ const details = failure?.error?.details;
78
+ const validation = Array.isArray(details) ? details.find((entry) =>
79
+ entry?.reason === 'VALIDATION_REQUIRED'
80
+ && typeof entry.metadata?.validation_url === 'string'
81
+ && entry.metadata.validation_url.length > 0) : null;
82
+ if (validation) {
83
+ throw accountVerificationError(validation.metadata.validation_url, failure.error.message, context.email);
84
+ }
85
+ const detail = _scrubTokens(text);
86
+ throw new Error(`[antigravity-oauth] ${action} failed: ${res.status} ${res.statusText}: ${detail}`);
87
+ }
88
+ const payload = await res.json();
89
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
90
+ throw new Error(`[antigravity-oauth] invalid ${action} response`);
91
+ }
92
+ return payload;
80
93
  }
81
94
 
82
- async function fetchAccountEmail(accessToken, { fetchFn = fetch } = {}) {
95
+ async function fetchAccountEmail(accessToken, { fetchFn = fetch, signal = null } = {}) {
83
96
  try {
84
97
  const res = await fetchFn(USERINFO_URL, {
85
98
  headers: { Authorization: `Bearer ${accessToken}` },
86
- signal: AbortSignal.timeout(TOKEN_TIMEOUT_MS),
99
+ redirect: 'error',
100
+ signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(TOKEN_TIMEOUT_MS)]) : AbortSignal.timeout(TOKEN_TIMEOUT_MS),
87
101
  });
88
102
  if (!res.ok) return '';
89
103
  const json = await res.json();
90
104
  return String(json?.email || '');
91
- } catch { return ''; }
105
+ } catch {
106
+ signal?.throwIfAborted();
107
+ return '';
108
+ }
109
+ }
110
+
111
+ async function loadProjectState(context) {
112
+ const body = { metadata: codeAssistMetadata() };
113
+ let payload = await requestCodeAssist('loadCodeAssist', body, context);
114
+ const projectId = extractProjectId(payload);
115
+ if (payload.paidTier == null && projectId) {
116
+ payload = await requestCodeAssist('loadCodeAssist', {
117
+ ...body,
118
+ cloudaicompanionProject: projectId,
119
+ }, context);
120
+ }
121
+ return payload;
122
+ }
123
+
124
+ function tierIds(list, key) {
125
+ return Array.isArray(list) ? list.map((tier) => tier?.[key]).filter((id) => typeof id === 'string' && id) : [];
126
+ }
127
+
128
+ function tierSummary(payload) {
129
+ return `allowed tiers: [${tierIds(payload.allowedTiers, 'id').join(', ')}]; `
130
+ + `ineligible tiers: [${tierIds(payload.ineligibleTiers, 'tierId').join(', ')}]`;
92
131
  }
93
132
 
94
133
  /**
95
- * Resolve the Cloud project backing this account, provisioning one when the
96
- * account has none. Endpoints are tried in order because only production
97
- * answers `loadCodeAssist` reliably for managed accounts.
134
+ * Tier to onboard with. Free tier when Google allows it (or reports no tier
135
+ * list at all); otherwise the default allowed tier that Google can provision
136
+ * without a user-supplied Cloud project, e.g. a paid subscription tier.
98
137
  */
99
- async function discoverProject(accessToken, { fetchFn = fetch, onProgress = null } = {}) {
100
- let tierId = 'free-tier';
101
- let loaded = false;
102
- let lastError = '';
103
- for (const endpoint of PROJECT_ENDPOINTS) {
104
- try {
105
- onProgress?.('Checking for an existing project...');
106
- const res = await postJson(
107
- `${endpoint}/v1internal:loadCodeAssist`,
108
- { metadata: codeAssistMetadata() },
109
- accessToken,
110
- { fetchFn },
111
- );
112
- if (!res.ok) {
113
- lastError = `${res.status} ${_scrubTokens(await res.text().catch(() => '')).slice(0, 200)}`;
114
- continue;
115
- }
116
- loaded = true;
117
- const payload = await res.json();
118
- const existing = extractProjectId(payload);
119
- if (existing) return existing;
120
- tierId = defaultTierId(payload?.allowedTiers, payload?.currentTier);
121
- break;
122
- } catch (err) {
123
- lastError = String(err?.message || err).slice(0, 200);
138
+ function onboardingTier(payload) {
139
+ const allowed = tierIds(payload.allowedTiers, 'id');
140
+ const ineligible = new Set(tierIds(payload.ineligibleTiers, 'tierId'));
141
+ if (allowed.includes('free-tier')) return 'free-tier';
142
+ if (!Array.isArray(payload.allowedTiers) && !ineligible.has('free-tier')) return 'free-tier';
143
+ const managed = (payload.allowedTiers ?? []).filter((tier) => typeof tier?.id === 'string' && tier.id
144
+ && tier.userDefinedCloudaicompanionProject !== true && !ineligible.has(tier.id));
145
+ return (managed.find((tier) => tier.isDefault) ?? managed[0])?.id ?? '';
146
+ }
147
+
148
+ function noTierError(payload, email) {
149
+ const summary = tierSummary(payload);
150
+ const blocked = payload.ineligibleTiers?.find((tier) => tier?.tierId === 'free-tier' && tier.reasonMessage)
151
+ ?? payload.ineligibleTiers?.find((tier) => tier?.reasonMessage);
152
+ if (blocked) {
153
+ const reason = `${blocked.reasonMessage}\n(${summary})`;
154
+ if (typeof blocked.validationUrl === 'string' && blocked.validationUrl) {
155
+ return accountVerificationError(blocked.validationUrl, reason, email);
124
156
  }
157
+ return new Error(`[antigravity-oauth] ${_scrubTokens(reason)}`);
125
158
  }
126
- if (!loaded && lastError) {
127
- throw new Error(`[antigravity-oauth] loadCodeAssist failed: ${lastError}`);
128
- }
159
+ return new Error(`[antigravity-oauth] loadCodeAssist allowed no tier to onboard (${summary})`);
160
+ }
129
161
 
130
- onProgress?.('Provisioning a project...');
131
- for (const endpoint of PROJECT_ENDPOINTS) {
132
- for (let attempt = 1; attempt <= ONBOARD_MAX_ATTEMPTS; attempt += 1) {
133
- try {
134
- const res = await postJson(
135
- `${endpoint}/v1internal:onboardUser`,
136
- { tierId, metadata: codeAssistMetadata() },
137
- accessToken,
138
- { fetchFn },
139
- );
140
- if (!res.ok) break;
141
- const payload = await res.json();
142
- const provisioned = extractProjectId(payload?.response) || extractProjectId(payload);
143
- if (payload?.done && provisioned) return provisioned;
144
- // The operation is long-running: poll the same endpoint.
145
- onProgress?.(`Waiting for project provisioning (${attempt}/${ONBOARD_MAX_ATTEMPTS})...`);
146
- await new Promise((r) => setTimeout(r, ONBOARD_INTERVAL_MS));
147
- } catch {
148
- break;
149
- }
162
+ async function provisionProject(context, tierId) {
163
+ const deadline = Date.now() + PROJECT_TIMEOUT_MS;
164
+ const remaining = () => {
165
+ const ms = deadline - Date.now();
166
+ if (ms <= 0) throw new Error(`[antigravity-oauth] onboardUser timed out after ${PROJECT_TIMEOUT_MS}ms`);
167
+ return ms;
168
+ };
169
+ let operation = await requestCodeAssist('onboardUser', {
170
+ tierId,
171
+ metadata: codeAssistMetadata(),
172
+ }, context, remaining());
173
+ while (operation.done !== true) {
174
+ await delay(Math.min(ONBOARD_INTERVAL_MS, remaining()), undefined, { signal: context.signal ?? undefined });
175
+ const timeoutMs = remaining();
176
+ if (typeof operation.name !== 'string' || !operation.name) {
177
+ throw new Error('[antigravity-oauth] onboardUser returned an operation without a name');
150
178
  }
179
+ operation = await requestCodeAssist(operation.name, undefined, context, timeoutMs);
180
+ }
181
+ if (operation.error != null) {
182
+ const { code, message } = operation.error;
183
+ const detail = message ? `${typeof code === 'number' ? `${code}: ` : ''}${message}` : JSON.stringify(operation.error);
184
+ throw new Error(`[antigravity-oauth] onboardUser operation failed: ${_scrubTokens(detail)}`);
185
+ }
186
+ if (!operation.response || typeof operation.response['@type'] !== 'string') {
187
+ throw new Error('[antigravity-oauth] invalid onboardUser response');
151
188
  }
152
- throw new Error('[antigravity-oauth] could not resolve a Cloud project for this account');
153
189
  }
154
190
 
155
- export async function exchangeAuthorizationCode({ code, verifier, fetchFn = fetch, onProgress = null }) {
191
+ /** Resolve account eligibility and return the project from a fresh status load. */
192
+ export async function discoverProject(accessToken, { fetchFn = fetch, onProgress = null, signal = null, email = '' } = {}) {
193
+ const context = { accessToken, fetchFn, signal, email };
194
+ onProgress?.('Checking Cloud Code Assist account status...');
195
+ const initial = await loadProjectState(context);
196
+ if (initial.currentTier == null) {
197
+ const tierId = onboardingTier(initial);
198
+ if (!tierId) throw noTierError(initial, email);
199
+ onProgress?.(`Provisioning the Antigravity ${tierId}...`);
200
+ await provisionProject(context, tierId);
201
+ }
202
+ onProgress?.('Refreshing Cloud Code Assist project...');
203
+ const refreshed = await loadProjectState(context);
204
+ const projectId = extractProjectId(refreshed);
205
+ if (!projectId) throw new Error('[antigravity-oauth] loadCodeAssist did not return a cloudaicompanionProject');
206
+ return projectId;
207
+ }
208
+
209
+ export async function exchangeAuthorizationCode({ code, verifier, fetchFn = fetch, onProgress = null, signal = null }) {
210
+ signal?.throwIfAborted();
156
211
  const cleanCode = String(code || '').trim();
157
212
  if (!cleanCode) throw new Error('[antigravity-oauth] authorization code is required');
158
213
  const res = await fetchFn(TOKEN_URL, {
@@ -169,7 +224,7 @@ export async function exchangeAuthorizationCode({ code, verifier, fetchFn = fetc
169
224
  // Secret-bearing (code + verifier): refuse redirects so neither can be
170
225
  // replayed against an untrusted host.
171
226
  redirect: 'error',
172
- signal: AbortSignal.timeout(TOKEN_TIMEOUT_MS),
227
+ signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(TOKEN_TIMEOUT_MS)]) : AbortSignal.timeout(TOKEN_TIMEOUT_MS),
173
228
  });
174
229
  if (!res.ok) {
175
230
  const text = await res.text().catch(() => '');
@@ -179,8 +234,8 @@ export async function exchangeAuthorizationCode({ code, verifier, fetchFn = fetc
179
234
  if (!json?.access_token || !json?.refresh_token) {
180
235
  throw new Error('[antigravity-oauth] token exchange response missing access_token or refresh_token');
181
236
  }
182
- const email = await fetchAccountEmail(json.access_token, { fetchFn });
183
- const projectId = await discoverProject(json.access_token, { fetchFn, onProgress });
237
+ const email = await fetchAccountEmail(json.access_token, { fetchFn, signal });
238
+ const projectId = await discoverProject(json.access_token, { fetchFn, onProgress, signal, email });
184
239
  const tokens = {
185
240
  access_token: json.access_token,
186
241
  refresh_token: json.refresh_token,
@@ -188,11 +243,18 @@ export async function exchangeAuthorizationCode({ code, verifier, fetchFn = fetc
188
243
  project_id: projectId,
189
244
  email,
190
245
  };
246
+ signal?.throwIfAborted();
191
247
  saveTokens(tokens);
192
248
  return tokens;
193
249
  }
194
250
 
195
- export async function beginOAuthLogin({ fetchFn = fetch } = {}) {
251
+ export async function beginOAuthLogin({
252
+ fetchFn = fetch,
253
+ onProgress = null,
254
+ createServerFn = createServer,
255
+ openBrowserFn = null,
256
+ } = {}) {
257
+ const controller = new AbortController();
196
258
  const pkce = generatePKCE();
197
259
  const state = randomBytes(16).toString('hex');
198
260
  const url = new URL(AUTH_URL);
@@ -210,53 +272,96 @@ export async function beginOAuthLogin({ fetchFn = fetch } = {}) {
210
272
  let server = null;
211
273
  let timeout = null;
212
274
  let finish = null;
275
+ let settled = false;
276
+ let exchangePromise = null;
277
+ const acceptCode = (code) => {
278
+ if (exchangePromise) return exchangePromise;
279
+ controller.signal.throwIfAborted();
280
+ // The browser deadline ends when a code arrives; network requests have
281
+ // their own deadlines. Browser and manual callbacks share one exchange.
282
+ if (timeout) clearTimeout(timeout);
283
+ timeout = null;
284
+ exchangePromise = exchangeAuthorizationCode({
285
+ code, verifier: pkce.verifier, fetchFn, onProgress, signal: controller.signal,
286
+ });
287
+ exchangePromise.then(
288
+ (tokens) => finish(tokens),
289
+ (error) => finish(null, error instanceof Error ? error : new Error(String(error))),
290
+ );
291
+ return exchangePromise;
292
+ };
213
293
  const waitForCallback = new Promise((resolvePromise, reject) => {
214
- let settled = false;
215
294
  finish = (value, error = null) => {
216
295
  if (settled) return;
217
296
  settled = true;
297
+ controller.abort();
218
298
  if (timeout) clearTimeout(timeout);
219
299
  try { server?.close(); } catch { /* already closed */ }
220
300
  if (error) reject(error);
221
301
  else resolvePromise(value);
222
302
  };
223
- server = createServer(async (req, res) => {
303
+ server = createServerFn(async (req, res) => {
224
304
  const requestUrl = new URL(req.url || '/', `http://${CALLBACK_HOST}:${CALLBACK_PORT}`);
225
305
  if (requestUrl.pathname !== CALLBACK_PATH) {
226
306
  res.writeHead(404);
227
307
  res.end();
228
308
  return;
229
309
  }
230
- const code = requestUrl.searchParams.get('code');
231
- if (!code || requestUrl.searchParams.get('state') !== state) {
310
+ if (requestUrl.searchParams.get('state') !== state) {
311
+ res.writeHead(400);
312
+ res.end('Invalid OAuth state');
313
+ return;
314
+ }
315
+ const authorizationError = requestUrl.searchParams.get('error');
316
+ if (authorizationError) {
317
+ const detail = requestUrl.searchParams.get('error_description') || authorizationError;
318
+ res.writeHead(400, { 'Content-Type': 'text/html' });
319
+ res.end(CALLBACK_FAILURE_HTML);
320
+ // A late denial must not cancel an already accepted code.
321
+ if (!exchangePromise) {
322
+ finish(null, new Error(`[antigravity-oauth] authorization failed: ${_scrubTokens(detail)}`));
323
+ }
324
+ return;
325
+ }
326
+ const code = requestUrl.searchParams.get('code')?.trim();
327
+ if (!code) {
232
328
  res.writeHead(400);
233
- res.end('Invalid');
234
- finish(null);
329
+ res.end('Missing authorization code');
235
330
  return;
236
331
  }
237
- res.writeHead(200, { 'Content-Type': 'text/html' });
238
- res.end('<html><body><h2>Antigravity login successful! You can close this tab.</h2></body></html>');
239
332
  try {
240
- finish(await exchangeAuthorizationCode({ code, verifier: pkce.verifier, fetchFn }));
241
- } catch (err) {
242
- finish(null, err instanceof Error ? err : new Error(String(err)));
333
+ await acceptCode(code);
334
+ res.writeHead(200, { 'Content-Type': 'text/html' });
335
+ res.end('<html><body><h2>Antigravity connected.</h2><p>You can close this tab.</p></body></html>');
336
+ } catch {
337
+ // The shared exchange reports its error through waitForCallback.
338
+ // Do not interpolate provider-controlled text into browser HTML.
339
+ res.writeHead(500, { 'Content-Type': 'text/html' });
340
+ res.end(CALLBACK_FAILURE_HTML);
243
341
  }
244
342
  });
245
- timeout = setTimeout(() => finish(null), LOGIN_TIMEOUT_MS);
343
+ timeout = setTimeout(() => finish(
344
+ null,
345
+ new Error(`[antigravity-oauth] browser authentication timed out after ${LOGIN_TIMEOUT_MS}ms`),
346
+ ), LOGIN_TIMEOUT_MS);
246
347
  if (timeout.unref) timeout.unref();
348
+ server.on('error', (err) => finish(
349
+ null,
350
+ new Error(`[antigravity-oauth] callback server failed on ${CALLBACK_HOST}:${CALLBACK_PORT}: ${err?.message || err}`),
351
+ ));
247
352
  server.listen(CALLBACK_PORT, CALLBACK_HOST, async () => {
353
+ if (settled) return;
248
354
  process.stderr.write(`\n[antigravity-oauth] Open this URL to log in:\n${url.toString()}\n\n`);
249
355
  try {
250
- const { openInBrowser } = await import('../../../shared/open-url.mjs');
251
- openInBrowser(url.toString());
356
+ if (openBrowserFn) await openBrowserFn(url.toString());
357
+ else {
358
+ const { openInBrowser } = await import('../../../shared/open-url.mjs');
359
+ if (!settled) await openInBrowser(url.toString());
360
+ }
252
361
  } catch (err) {
253
362
  process.stderr.write(`[antigravity-oauth] browser open failed: ${String(err?.message || err).slice(0, 200)}\n`);
254
363
  }
255
364
  });
256
- server.on('error', (err) => finish(
257
- null,
258
- new Error(`[antigravity-oauth] callback server failed on ${CALLBACK_HOST}:${CALLBACK_PORT}: ${err?.message || err}`),
259
- ));
260
365
  });
261
366
 
262
367
  return {
@@ -264,11 +369,10 @@ export async function beginOAuthLogin({ fetchFn = fetch } = {}) {
264
369
  url: url.toString(),
265
370
  waitForCallback,
266
371
  completeCode: async (input) => {
267
- const parsed = parseOAuthCodeInput(input);
372
+ const parsed = parseOAuthCodeInput(input, { allowHashState: true });
268
373
  if (parsed.state && parsed.state !== state) throw new Error('[antigravity-oauth] OAuth state mismatch');
269
- const tokens = await exchangeAuthorizationCode({ code: parsed.code, verifier: pkce.verifier, fetchFn });
270
- finish?.(tokens);
271
- return tokens;
374
+ if (!parsed.code) throw new Error('[antigravity-oauth] authorization code is required');
375
+ return await acceptCode(parsed.code);
272
376
  },
273
377
  cancel: () => { finish?.(null); },
274
378
  };
@@ -19,6 +19,8 @@ import { getPluginData } from '../config.mjs';
19
19
  import { writeJsonAtomicSync } from '../../../shared/atomic-file.mjs';
20
20
  import { boundProviderAuthPath } from '../../../shared/provider-auth-binding.mjs';
21
21
  import { scrubOAuthSecrets } from './lib/oauth-token-utils.mjs';
22
+ import { ANTIGRAVITY_MODELS } from './provider-model-identities.mjs';
23
+ export { ANTIGRAVITY_MODELS } from './provider-model-identities.mjs';
22
24
 
23
25
  // The Antigravity IDE's installed-app OAuth client, which every copy of that
24
26
  // IDE ships (a native-app client is not a confidential credential). It is
@@ -35,7 +37,7 @@ export const CLIENT_SECRET = decode('R09DU1BYLUs1', 'OEZXUjQ4NkxkTEoxbUxCOHNYQzR
35
37
 
36
38
  export const AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth';
37
39
  export const TOKEN_URL = 'https://oauth2.googleapis.com/token';
38
- export const USERINFO_URL = 'https://www.googleapis.com/oauth2/v2/userinfo';
40
+ export const USERINFO_URL = 'https://www.googleapis.com/oauth2/v1/userinfo?alt=json';
39
41
  export const SCOPES = [
40
42
  'https://www.googleapis.com/auth/cloud-platform',
41
43
  'https://www.googleapis.com/auth/userinfo.email',
@@ -47,74 +49,102 @@ export const SCOPES = [
47
49
  export const CALLBACK_HOST = '127.0.0.1';
48
50
  export const CALLBACK_PORT = 51121;
49
51
  export const CALLBACK_PATH = '/oauth-callback';
50
- // Google matches the redirect URI string exactly, and the registered client is
51
- // bound to `localhost` — the loopback server binds 127.0.0.1 all the same.
52
- export const REDIRECT_URI = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
52
+ // Use the same literal loopback address in authorization and token exchange.
53
+ export const REDIRECT_URI = `http://${CALLBACK_HOST}:${CALLBACK_PORT}${CALLBACK_PATH}`;
53
54
 
54
- // Content requests ride the IDE's daily sandbox channel first. Production is
55
- // kept last as a fallback: it answers, but not for every Antigravity-only model.
56
- const ENDPOINT_DAILY = 'https://daily-cloudcode-pa.sandbox.googleapis.com';
55
+ // Content requests ride the IDE's own daily channel first (the host the
56
+ // shipped client is launched with). The sandbox daily host is an older
57
+ // channel that has answered with version-gating stubs; autopush is the
58
+ // fastest alternate when daily is saturated. Production is kept last: it
59
+ // answers, but not for every Antigravity-only model.
60
+ const ENDPOINT_DAILY = 'https://daily-cloudcode-pa.googleapis.com';
61
+ const ENDPOINT_DAILY_SANDBOX = 'https://daily-cloudcode-pa.sandbox.googleapis.com';
57
62
  const ENDPOINT_AUTOPUSH = 'https://autopush-cloudcode-pa.sandbox.googleapis.com';
58
63
  const ENDPOINT_PROD = 'https://cloudcode-pa.googleapis.com';
59
- export const CONTENT_ENDPOINTS = Object.freeze([ENDPOINT_DAILY, ENDPOINT_AUTOPUSH, ENDPOINT_PROD]);
60
- // Project discovery is best supported on production, so it leads there.
61
- export const PROJECT_ENDPOINTS = Object.freeze([ENDPOINT_PROD, ENDPOINT_DAILY, ENDPOINT_AUTOPUSH]);
64
+ export const CONTENT_ENDPOINTS = Object.freeze([ENDPOINT_DAILY, ENDPOINT_AUTOPUSH, ENDPOINT_DAILY_SANDBOX, ENDPOINT_PROD]);
65
+ // Account provisioning uses the hub control plane, not the content sandboxes.
66
+ export const PROJECT_ENDPOINT = ENDPOINT_DAILY;
62
67
 
63
- // The gateway exposes no public catalog endpoint, so the model list is curated.
64
- // Wire ids are the ones the backend accepts verbatim; Gemini 3 Pro and the
65
- // Claude family carry their effort tier in the id rather than a parameter.
66
- export const ANTIGRAVITY_MODELS = Object.freeze([
67
- { id: 'gemini-3-pro-high', name: 'Gemini 3 Pro High', provider: 'antigravity-oauth', contextWindow: 1048576 },
68
- { id: 'gemini-3-pro-low', name: 'Gemini 3 Pro Low', provider: 'antigravity-oauth', contextWindow: 1048576 },
69
- { id: 'gemini-3-flash-agent', name: 'Gemini 3 Flash', provider: 'antigravity-oauth', contextWindow: 1048576 },
70
- { id: 'claude-opus-4-6-thinking', name: 'Claude Opus 4.6 Thinking', provider: 'antigravity-oauth', contextWindow: 250000 },
71
- { id: 'claude-sonnet-4-6', name: 'Claude Sonnet 4.6', provider: 'antigravity-oauth', contextWindow: 250000 },
72
- { id: 'claude-opus-4-5-thinking', name: 'Claude Opus 4.5 Thinking', provider: 'antigravity-oauth', contextWindow: 200000 },
73
- { id: 'claude-sonnet-4-5', name: 'Claude Sonnet 4.5', provider: 'antigravity-oauth', contextWindow: 1000000 },
74
- ]);
75
68
  export const DEFAULT_ANTIGRAVITY_MODEL = ANTIGRAVITY_MODELS[0].id;
76
69
 
77
- const ANTIGRAVITY_VERSION_FALLBACK = '1.18.3';
70
+ const ANTIGRAVITY_VERSION_FALLBACK = '2.8.0';
78
71
  export const LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
79
72
  export const TOKEN_TIMEOUT_MS = 30_000;
80
73
  export const PROJECT_TIMEOUT_MS = 30_000;
81
74
  // Refresh ahead of expiry so an in-flight turn never posts a stale token.
82
75
  export const TOKEN_REFRESH_SKEW_MS = 5 * 60 * 1000;
83
76
 
77
+ // The backend gates models on the client version in the User-Agent, so the
78
+ // version tracks the latest Antigravity release via its update manifest.
79
+ // The pinned value is the offline fallback; the env override always wins.
80
+ const VERSION_MANIFEST_URL = 'https://antigravity-hub-auto-updater-974169037036.us-central1.run.app/manifest/latest-arm64-mac.yml';
81
+ const VERSION_FETCH_TIMEOUT_MS = 5_000;
82
+ const VERSION_RETRY_AFTER_MS = 10 * 60 * 1000;
83
+ let _discoveredVersion = null;
84
+ let _versionFetch = null;
85
+ let _versionFailedAt = 0;
86
+
84
87
  function antigravityVersion() {
85
- return String(process.env.MIXDOG_ANTIGRAVITY_VERSION || '').trim() || ANTIGRAVITY_VERSION_FALLBACK;
88
+ return String(process.env.MIXDOG_ANTIGRAVITY_VERSION || '').trim() || _discoveredVersion || ANTIGRAVITY_VERSION_FALLBACK;
89
+ }
90
+
91
+ /** Version from an electron-builder update manifest, or null when absent. */
92
+ export function parseAntigravityManifestVersion(text) {
93
+ for (const line of String(text || '').split(/\r?\n/)) {
94
+ const match = /^\s*version\s*:\s*(?:"([^"]*)"|'([^']*)'|([^\s#]+))\s*(?:#.*)?$/.exec(line);
95
+ if (!match) continue;
96
+ const version = (match[1] ?? match[2] ?? match[3] ?? '').trim();
97
+ return /^\d+\.\d+\.\d+$/.test(version) ? version : null;
98
+ }
99
+ return null;
100
+ }
101
+
102
+ /**
103
+ * Resolve the current client version once per process. A failed lookup keeps
104
+ * the pinned fallback and is not retried for a while, so an unreachable
105
+ * manifest host never delays every request.
106
+ */
107
+ export function ensureAntigravityVersion({ fetchFn = fetch } = {}) {
108
+ if (process.env.MIXDOG_ANTIGRAVITY_VERSION || _discoveredVersion) return Promise.resolve(antigravityVersion());
109
+ if (_versionFetch) return _versionFetch;
110
+ if (Date.now() - _versionFailedAt < VERSION_RETRY_AFTER_MS) return Promise.resolve(antigravityVersion());
111
+ _versionFetch = (async () => {
112
+ try {
113
+ const res = await fetchFn(VERSION_MANIFEST_URL, {
114
+ headers: { 'Cache-Control': 'no-cache', 'User-Agent': 'electron-builder' },
115
+ signal: AbortSignal.timeout(VERSION_FETCH_TIMEOUT_MS),
116
+ });
117
+ if (res.ok) _discoveredVersion = parseAntigravityManifestVersion(await res.text());
118
+ } catch {
119
+ // The pinned fallback stays valid when discovery fails.
120
+ } finally {
121
+ if (!_discoveredVersion) _versionFailedAt = Date.now();
122
+ _versionFetch = null;
123
+ }
124
+ return antigravityVersion();
125
+ })();
126
+ return _versionFetch;
86
127
  }
87
128
 
88
- function osPlatformTag() {
89
- return process.platform === 'win32' ? 'WINDOWS' : process.platform === 'darwin' ? 'MACOS' : 'LINUX';
129
+ export function _resetAntigravityVersionForTest() {
130
+ _discoveredVersion = null;
131
+ _versionFetch = null;
132
+ _versionFailedAt = 0;
90
133
  }
91
134
 
92
135
  /**
93
- * Headers the Antigravity IDE sends. The backend gates newer models on the
94
- * client version, so this tracks the shipped release.
136
+ * Cloud Code Assist expects the hub client identity rather than Electron's
137
+ * browser headers. Keep the captured client platform independent of the host.
95
138
  */
96
139
  export function antigravityHeaders() {
97
140
  return {
98
- 'User-Agent': `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) `
99
- + `Antigravity/${antigravityVersion()} Chrome/138.0.7204.235 Electron/37.3.1 Safari/537.36`,
100
- 'X-Goog-Api-Client': 'google-cloud-sdk vscode_cloudshelleditor/0.1',
101
- 'Client-Metadata': JSON.stringify({
102
- ideType: 'ANTIGRAVITY',
103
- platform: osPlatformTag(),
104
- pluginType: 'GEMINI',
105
- }),
141
+ 'User-Agent': `antigravity/hub/${antigravityVersion()} (aidev_client; os_type=darwin; arch=arm64; cl=963137146)`,
106
142
  };
107
143
  }
108
144
 
109
145
  /** `loadCodeAssist` / `onboardUser` metadata block. */
110
- export function codeAssistMetadata(projectId = '') {
111
- const metadata = {
112
- ideType: 'ANTIGRAVITY',
113
- platform: osPlatformTag(),
114
- pluginType: 'GEMINI',
115
- };
116
- if (projectId) metadata.duetProject = projectId;
117
- return metadata;
146
+ export function codeAssistMetadata() {
147
+ return { ideType: 'ANTIGRAVITY' };
118
148
  }
119
149
 
120
150
  // --- Token store ---