opencode-antigravity-auth 2.0.0 → 2.1.1
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.
- package/README.md +32 -710
- package/dist/index.d.ts +1 -1
- package/dist/src/plugin/account-management.d.ts +67 -0
- package/dist/src/plugin/account-management.d.ts.map +1 -0
- package/dist/src/plugin/account-management.js +174 -0
- package/dist/src/plugin/account-management.js.map +1 -0
- package/dist/src/plugin/account-rpc.d.ts +70 -0
- package/dist/src/plugin/account-rpc.d.ts.map +1 -0
- package/dist/src/plugin/account-rpc.js +28 -0
- package/dist/src/plugin/account-rpc.js.map +1 -0
- package/dist/src/plugin/accounts.d.ts +23 -9
- package/dist/src/plugin/accounts.d.ts.map +1 -1
- package/dist/src/plugin/accounts.js +190 -22
- package/dist/src/plugin/accounts.js.map +1 -1
- package/dist/src/plugin/config/models-v2.d.ts +19 -1
- package/dist/src/plugin/config/models-v2.d.ts.map +1 -1
- package/dist/src/plugin/config/models-v2.js +49 -38
- package/dist/src/plugin/config/models-v2.js.map +1 -1
- package/dist/src/plugin/quota.d.ts +33 -1
- package/dist/src/plugin/quota.d.ts.map +1 -1
- package/dist/src/plugin/quota.js +43 -18
- package/dist/src/plugin/quota.js.map +1 -1
- package/dist/src/plugin/refresh-queue.d.ts +4 -1
- package/dist/src/plugin/refresh-queue.d.ts.map +1 -1
- package/dist/src/plugin/refresh-queue.js +21 -4
- package/dist/src/plugin/refresh-queue.js.map +1 -1
- package/dist/src/plugin/storage.d.ts +15 -0
- package/dist/src/plugin/storage.d.ts.map +1 -1
- package/dist/src/plugin/storage.js +61 -2
- package/dist/src/plugin/storage.js.map +1 -1
- package/dist/src/plugin/token.d.ts +1 -3
- package/dist/src/plugin/token.d.ts.map +1 -1
- package/dist/src/plugin/token.js +43 -3
- package/dist/src/plugin/token.js.map +1 -1
- package/dist/src/plugin/transform/model-resolver.d.ts +2 -5
- package/dist/src/plugin/transform/model-resolver.d.ts.map +1 -1
- package/dist/src/plugin/transform/model-resolver.js +9 -1
- package/dist/src/plugin/transform/model-resolver.js.map +1 -1
- package/dist/src/plugin/types.d.ts +5 -0
- package/dist/src/plugin/types.d.ts.map +1 -1
- package/dist/src/plugin.d.ts +3 -3
- package/dist/src/plugin.d.ts.map +1 -1
- package/dist/src/plugin.js +84 -26
- package/dist/src/plugin.js.map +1 -1
- package/dist/src/tui.d.ts +64 -0
- package/dist/src/tui.d.ts.map +1 -0
- package/dist/src/tui.js +124 -0
- package/dist/src/tui.js.map +1 -0
- package/dist/src/v2.d.ts.map +1 -1
- package/dist/src/v2.js +444 -112
- package/dist/src/v2.js.map +1 -1
- package/package.json +9 -3
- package/tui.js +1 -0
package/dist/src/v2.js
CHANGED
|
@@ -5,14 +5,18 @@ import { createGoogleGenerativeAI } from "@ai-sdk/google";
|
|
|
5
5
|
import { Credential, Integration, Model, Plugin, Provider } from "@opencode/plugin";
|
|
6
6
|
import { authorizeAntigravity, exchangeAntigravity } from "./antigravity/oauth.js";
|
|
7
7
|
import { ANTIGRAVITY_REDIRECT_URI } from "./constants.js";
|
|
8
|
-
import { createAntigravityPlugin
|
|
8
|
+
import { createAntigravityPlugin } from "./plugin.js";
|
|
9
9
|
import { accessTokenExpired, formatRefreshParts, parseRefreshParts } from "./plugin/auth.js";
|
|
10
|
-
import { OPENCODE_V2_MODEL_DEFINITIONS } from "./plugin/config/models-v2.js";
|
|
10
|
+
import { OPENCODE_V2_MODEL_DEFINITIONS, buildAccountModelDefinitions, ModelCatalogCacheSchema } from "./plugin/config/models-v2.js";
|
|
11
|
+
import { accountID, accountPoolIdentity, accountCredentialsIdentity, loadManagedAccounts, registerAccountManagement } from "./plugin/account-management.js";
|
|
12
|
+
import { checkAccountsQuota } from "./plugin/quota.js";
|
|
13
|
+
import { createLogger } from "./plugin/logger.js";
|
|
11
14
|
import { ensureProjectContext } from "./plugin/project.js";
|
|
12
15
|
import { executeSearch } from "./plugin/search.js";
|
|
13
16
|
import { startOAuthListener } from "./plugin/server.js";
|
|
14
|
-
import { loadAccounts,
|
|
17
|
+
import { loadAccounts, matchesAccountToken, saveAccountCredentials, updateAccountCredentials, updateAccounts } from "./plugin/storage.js";
|
|
15
18
|
import { AntigravityTokenRefreshError, refreshAccessToken } from "./plugin/token.js";
|
|
19
|
+
import { resolveModelWithTier } from "./plugin/transform/model-resolver.js";
|
|
16
20
|
const PROVIDER_ID = "google";
|
|
17
21
|
const METHOD_ID = "antigravity";
|
|
18
22
|
// Core rewrites the standard Google SDK package to its native transport after
|
|
@@ -60,8 +64,15 @@ function oauthCredential(auth, metadata) {
|
|
|
60
64
|
...(metadata ? { metadata } : {}),
|
|
61
65
|
};
|
|
62
66
|
}
|
|
63
|
-
|
|
64
|
-
|
|
67
|
+
/** Recognizes Antigravity grants and V1 OAuth imports bound to this account pool. */
|
|
68
|
+
function isAntigravityCredential(value, accounts = [], boundID) {
|
|
69
|
+
if (value?.type !== "oauth" || !value.refresh)
|
|
70
|
+
return false;
|
|
71
|
+
if (value.methodID === METHOD_ID)
|
|
72
|
+
return true;
|
|
73
|
+
// V1 migration imports generic OAuth credentials; the pool establishes ownership.
|
|
74
|
+
return value.methodID === "oauth" && (typeof boundID === "string"
|
|
75
|
+
|| accounts.some(account => matchesAccountToken(account, parseRefreshParts(value.refresh).refreshToken)));
|
|
65
76
|
}
|
|
66
77
|
/** OpenCode 2.1's source transforms and AI SDK 3 transport adapter. */
|
|
67
78
|
export const AntigravityV2Plugin = Plugin.define({
|
|
@@ -69,10 +80,16 @@ export const AntigravityV2Plugin = Plugin.define({
|
|
|
69
80
|
async setup(ctx) {
|
|
70
81
|
const client = transportClient();
|
|
71
82
|
const outbound = new AsyncLocalStorage();
|
|
83
|
+
let modelAccounts = {};
|
|
84
|
+
let accountCatalogLoaded = false;
|
|
72
85
|
const transport = await createAntigravityPlugin(PROVIDER_ID)({
|
|
73
86
|
client,
|
|
74
87
|
directory: ctx.location.directory,
|
|
75
88
|
runtimeVersion: 2,
|
|
89
|
+
canUseAccount: (account, model) => {
|
|
90
|
+
const eligible = modelAccounts[`antigravity-${resolveModelWithTier(model).actualModel}`];
|
|
91
|
+
return !accountCatalogLoaded || !!eligible?.includes(accountID(account));
|
|
92
|
+
},
|
|
76
93
|
fetch: (input, init) => {
|
|
77
94
|
const send = outbound.getStore() ?? globalThis.fetch;
|
|
78
95
|
// Core's wrapper applies model.body to string init.body. It has already
|
|
@@ -84,16 +101,124 @@ export const AntigravityV2Plugin = Plugin.define({
|
|
|
84
101
|
},
|
|
85
102
|
});
|
|
86
103
|
const registrations = [];
|
|
87
|
-
const saved = await
|
|
88
|
-
|
|
104
|
+
const saved = await loadManagedAccounts();
|
|
105
|
+
let hasSavedAccounts = saved?.accounts.some((account) => account.enabled !== false && !!account.refreshToken);
|
|
106
|
+
const log = createLogger("v2");
|
|
107
|
+
const cachedResult = ModelCatalogCacheSchema.safeParse(await ctx.storage.get("model-catalog"));
|
|
108
|
+
const cached = cachedResult.success ? cachedResult.data : undefined;
|
|
109
|
+
const validCache = cached?.identity === accountPoolIdentity(saved?.accounts ?? []) && cached.models;
|
|
110
|
+
let modelDefinitions = validCache ? buildAccountModelDefinitions(validCache) : OPENCODE_V2_MODEL_DEFINITIONS;
|
|
111
|
+
accountCatalogLoaded = !!validCache;
|
|
112
|
+
if (validCache)
|
|
113
|
+
modelAccounts = cached.accounts;
|
|
114
|
+
let discoveryGeneration = 0;
|
|
115
|
+
let catalogSnapshot;
|
|
116
|
+
const catalogWrites = new Set();
|
|
117
|
+
const discoveries = new Set();
|
|
118
|
+
const writeCatalog = async () => {
|
|
119
|
+
let pending = catalogSnapshot;
|
|
120
|
+
while (pending !== undefined) {
|
|
121
|
+
await ctx.storage.set("model-catalog", pending);
|
|
122
|
+
if (pending === catalogSnapshot)
|
|
123
|
+
return;
|
|
124
|
+
pending = catalogSnapshot;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
const discoverModels = async () => {
|
|
128
|
+
const discovery = ++discoveryGeneration;
|
|
129
|
+
await resolveAuth();
|
|
130
|
+
const stored = await loadManagedAccounts();
|
|
131
|
+
const accounts = stored?.accounts.filter(account => account.enabled !== false) ?? [];
|
|
132
|
+
hasSavedAccounts = accounts.length > 0;
|
|
133
|
+
if (!accounts.length)
|
|
134
|
+
throw new Error("No enabled Google accounts. Add or enable an account first.");
|
|
135
|
+
const results = await checkAccountsQuota(accounts, client, PROVIDER_ID, {
|
|
136
|
+
onAccountUpdated: saveAccountCredentials,
|
|
137
|
+
signal: lifetime.signal,
|
|
138
|
+
});
|
|
139
|
+
const successful = results.filter(result => result.quota?.models && Object.keys(result.quota.models).length);
|
|
140
|
+
const models = {};
|
|
141
|
+
const eligibleAccounts = {};
|
|
142
|
+
if (discovery === discoveryGeneration) {
|
|
143
|
+
invalidateLoader();
|
|
144
|
+
await transport.flush?.();
|
|
145
|
+
}
|
|
146
|
+
const latest = await updateAccounts(latest => {
|
|
147
|
+
for (const result of results) {
|
|
148
|
+
const original = accounts[result.index];
|
|
149
|
+
if (!original)
|
|
150
|
+
continue;
|
|
151
|
+
const account = latest.accounts.find(item => accountID(item) === accountID(original));
|
|
152
|
+
if (!account)
|
|
153
|
+
continue;
|
|
154
|
+
if (result.updatedAccount && account.refreshToken === original.refreshToken) {
|
|
155
|
+
updateAccountCredentials(account, result.updatedAccount);
|
|
156
|
+
}
|
|
157
|
+
if (account.enabled === false)
|
|
158
|
+
continue;
|
|
159
|
+
if (!result.quota?.models)
|
|
160
|
+
continue;
|
|
161
|
+
for (const id of Object.keys(buildAccountModelDefinitions(result.quota.models))) {
|
|
162
|
+
(eligibleAccounts[id] ??= []).push(accountID(account));
|
|
163
|
+
}
|
|
164
|
+
for (const [id, model] of Object.entries(result.quota?.models ?? {})) {
|
|
165
|
+
const usable = (value) => !value.disabled && value.selectable !== false;
|
|
166
|
+
if (!models[id] || !usable(models[id]) || usable(model))
|
|
167
|
+
models[id] = model;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
// Credentials remain valuable after a concurrent pool change, but only
|
|
172
|
+
// the newest complete discovery for this pool may replace the catalog.
|
|
173
|
+
if (disposed || discovery !== discoveryGeneration || accountPoolIdentity(latest?.accounts ?? []) !== accountPoolIdentity(accounts))
|
|
174
|
+
return Object.keys(modelDefinitions).length;
|
|
175
|
+
if (!successful.length)
|
|
176
|
+
throw new Error(results.find(result => result.error)?.error ?? "Google did not return a model catalog. Check account access and try again.");
|
|
177
|
+
modelDefinitions = buildAccountModelDefinitions(models);
|
|
178
|
+
modelAccounts = eligibleAccounts;
|
|
179
|
+
accountCatalogLoaded = true;
|
|
180
|
+
catalogSnapshot = JSON.parse(JSON.stringify({ version: 3, identity: accountPoolIdentity(latest?.accounts ?? []), updatedAt: Date.now(), models, accounts: eligibleAccounts }));
|
|
181
|
+
const writing = writeCatalog();
|
|
182
|
+
catalogWrites.add(writing);
|
|
183
|
+
try {
|
|
184
|
+
await writing;
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
catalogWrites.delete(writing);
|
|
188
|
+
}
|
|
189
|
+
await ctx.provider.reload();
|
|
190
|
+
return Object.keys(modelDefinitions).length;
|
|
191
|
+
};
|
|
192
|
+
const refreshModels = () => {
|
|
193
|
+
if (disposed)
|
|
194
|
+
return Promise.reject(new Error("Antigravity plugin has been disposed"));
|
|
195
|
+
const discovering = discoverModels();
|
|
196
|
+
discoveries.add(discovering);
|
|
197
|
+
void discovering.then(() => discoveries.delete(discovering), () => discoveries.delete(discovering));
|
|
198
|
+
return discovering;
|
|
199
|
+
};
|
|
89
200
|
const listeners = new Set();
|
|
90
201
|
const lifetime = new AbortController();
|
|
202
|
+
let requestLifetime = new AbortController();
|
|
91
203
|
let disposed = false;
|
|
204
|
+
const credentialOperations = new Set();
|
|
205
|
+
const runCredentialOperation = (operation) => {
|
|
206
|
+
if (disposed)
|
|
207
|
+
return Promise.reject(new Error("Antigravity plugin has been disposed"));
|
|
208
|
+
const running = operation();
|
|
209
|
+
credentialOperations.add(running);
|
|
210
|
+
void running.then(() => credentialOperations.delete(running), () => credentialOperations.delete(running));
|
|
211
|
+
return running;
|
|
212
|
+
};
|
|
92
213
|
let loader;
|
|
93
214
|
let loaderIdentity;
|
|
94
215
|
let generation = 0;
|
|
95
216
|
let observedIdentity;
|
|
96
217
|
let loading;
|
|
218
|
+
let usesAntigravityOAuth = false;
|
|
219
|
+
let modelReloadPending = false;
|
|
220
|
+
let connectionEvents;
|
|
221
|
+
let modelAccessRefresh = Promise.resolve();
|
|
97
222
|
const observeAuth = (resolved) => {
|
|
98
223
|
if (observedIdentity !== resolved?.identity) {
|
|
99
224
|
observedIdentity = resolved?.identity;
|
|
@@ -105,18 +230,33 @@ export const AntigravityV2Plugin = Plugin.define({
|
|
|
105
230
|
};
|
|
106
231
|
const resolveAuth = async () => {
|
|
107
232
|
const connection = await ctx.integration.connection.active(PROVIDER_ID);
|
|
233
|
+
let poolSelection = await ctx.storage.get("pool-selection");
|
|
234
|
+
const connectionID = connection?.type === "credential" ? connection.id : "pool";
|
|
235
|
+
if (poolSelection && poolSelection !== connectionID) {
|
|
236
|
+
await ctx.storage.remove("pool-selection");
|
|
237
|
+
poolSelection = undefined;
|
|
238
|
+
}
|
|
108
239
|
if (connection) {
|
|
109
240
|
const value = await ctx.integration.connection.resolve(connection);
|
|
110
241
|
const stored = await loadAccounts();
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
if (!isAntigravityCredential(value
|
|
114
|
-
return observeAuth(undefined);
|
|
115
|
-
if (value?.type !== "oauth")
|
|
242
|
+
const bindingKey = connection.type === "credential" ? `connection-account:${connection.id}` : undefined;
|
|
243
|
+
const boundID = bindingKey ? await ctx.storage.get(bindingKey) : undefined;
|
|
244
|
+
if (!isAntigravityCredential(value, stored?.accounts, boundID))
|
|
116
245
|
return observeAuth(undefined);
|
|
117
246
|
// V1 credentials are imported with a generic method ID. Only matching
|
|
118
247
|
// this plugin's existing pool establishes their Antigravity ownership.
|
|
119
|
-
|
|
248
|
+
const metadataID = value.metadata?.antigravityAccountId;
|
|
249
|
+
const account = stored?.accounts.find(item => matchesAccountToken(item, parseRefreshParts(value.refresh).refreshToken))
|
|
250
|
+
?? stored?.accounts.find(item => (typeof metadataID === "string" && item.id === metadataID) || (typeof boundID === "string" && item.id === boundID) || (typeof value.metadata?.email === "string" && item.email === value.metadata.email));
|
|
251
|
+
if (account && bindingKey)
|
|
252
|
+
await ctx.storage.set(bindingKey, accountID(account));
|
|
253
|
+
const knownAccountRemoved = (typeof metadataID === "string" || typeof boundID === "string") && !account;
|
|
254
|
+
if (account?.enabled !== false && poolSelection !== "pool" && (connection.type !== "credential" || poolSelection !== connection.id)) {
|
|
255
|
+
if (!knownAccountRemoved) {
|
|
256
|
+
const auth = account ? { ...value, refresh: formatRefreshParts(account) } : value;
|
|
257
|
+
return observeAuth({ auth, identity: `credential:${parseRefreshParts(auth.refresh).refreshToken}:${accountCredentialsIdentity(stored?.accounts ?? [])}`, select: true, accountID: account && accountID(account) });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
120
260
|
}
|
|
121
261
|
// The independent pool also supports V1 migration without a connection.
|
|
122
262
|
const stored = await loadAccounts();
|
|
@@ -126,22 +266,80 @@ export const AntigravityV2Plugin = Plugin.define({
|
|
|
126
266
|
: stored?.accounts.find((item) => item.enabled !== false && item.refreshToken);
|
|
127
267
|
return observeAuth(account ? {
|
|
128
268
|
auth: { type: "oauth", refresh: formatRefreshParts(account) },
|
|
129
|
-
|
|
269
|
+
// Cursor changes preserve rotation state; credential rotation rebuilds
|
|
270
|
+
// a manager before it can attempt to refresh an obsolete token.
|
|
271
|
+
identity: `account-pool:${accountCredentialsIdentity(stored?.accounts ?? [])}`,
|
|
130
272
|
select: false,
|
|
273
|
+
accountID: accountID(account),
|
|
131
274
|
} : undefined);
|
|
132
275
|
};
|
|
133
276
|
const currentAuth = async () => (await resolveAuth())?.auth;
|
|
134
|
-
|
|
135
|
-
const
|
|
277
|
+
/** Reconciles credential ownership and retries catalog reloads until one succeeds. */
|
|
278
|
+
const refreshModelAccess = (reload) => {
|
|
279
|
+
// Read changes in order, including switches during the initial resolution.
|
|
280
|
+
modelAccessRefresh = modelAccessRefresh.then(async () => {
|
|
281
|
+
try {
|
|
282
|
+
const connection = await ctx.integration.connection.active(PROVIDER_ID);
|
|
283
|
+
const value = connection ? await ctx.integration.connection.resolve(connection) : undefined;
|
|
284
|
+
const stored = value?.type === "oauth" && value.methodID === "oauth" ? await loadAccounts() : undefined;
|
|
285
|
+
const boundID = connection?.type === "credential" ? await ctx.storage.get(`connection-account:${connection.id}`) : undefined;
|
|
286
|
+
const next = isAntigravityCredential(value, stored?.accounts, boundID);
|
|
287
|
+
if (disposed || (next === usesAntigravityOAuth && !modelReloadPending))
|
|
288
|
+
return;
|
|
289
|
+
usesAntigravityOAuth = next;
|
|
290
|
+
if (reload) {
|
|
291
|
+
modelReloadPending = true;
|
|
292
|
+
await ctx.model.reload();
|
|
293
|
+
modelReloadPending = false;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
if (!disposed)
|
|
298
|
+
log.warn("Could not update Google model authentication", { error: String(error) });
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
return modelAccessRefresh;
|
|
302
|
+
};
|
|
303
|
+
const invalidateLoader = (abortRequests = true) => {
|
|
304
|
+
generation += 1;
|
|
305
|
+
if (abortRequests) {
|
|
306
|
+
requestLifetime.abort(new Error("Antigravity accounts changed"));
|
|
307
|
+
requestLifetime = new AbortController();
|
|
308
|
+
}
|
|
309
|
+
loader = undefined;
|
|
310
|
+
loaderIdentity = undefined;
|
|
311
|
+
};
|
|
312
|
+
const rememberAuth = async (auth, email, previous) => {
|
|
136
313
|
const parts = parseRefreshParts(auth.refresh);
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
314
|
+
if (previous) {
|
|
315
|
+
let remembered = false;
|
|
316
|
+
const originalToken = parseRefreshParts(previous.refresh).refreshToken;
|
|
317
|
+
const saved = await updateAccounts(storage => {
|
|
318
|
+
const account = storage.accounts.find(item => matchesAccountToken(item, originalToken) || (typeof previous.metadata?.antigravityAccountId === "string" && item.id === previous.metadata.antigravityAccountId));
|
|
319
|
+
if (!account)
|
|
320
|
+
return;
|
|
321
|
+
remembered = true;
|
|
322
|
+
if (account.refreshToken === originalToken) {
|
|
323
|
+
updateAccountCredentials(account, parts);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
if (remembered)
|
|
327
|
+
return saved?.accounts.find(item => item.refreshToken === parts.refreshToken || item.id === previous.metadata?.antigravityAccountId);
|
|
328
|
+
// A saved native credential may outlive an explicitly deleted pool entry.
|
|
329
|
+
if (previous.metadata?.antigravityAccountId)
|
|
330
|
+
return undefined;
|
|
331
|
+
}
|
|
332
|
+
const saved = await updateAccounts(storage => {
|
|
333
|
+
const account = storage.accounts.find(item => matchesAccountToken(item, parts.refreshToken) || (!!email && item.email === email));
|
|
334
|
+
if (account) {
|
|
335
|
+
updateAccountCredentials(account, parts);
|
|
336
|
+
account.email = email ?? account.email;
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
storage.accounts.push({ id: accountID({ refreshToken: parts.refreshToken }), refreshToken: parts.refreshToken, projectId: parts.projectId, managedProjectId: parts.managedProjectId, email, enabled: true, addedAt: Date.now(), lastUsed: 0 });
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
return saved?.accounts.find(item => item.refreshToken === parts.refreshToken);
|
|
145
343
|
};
|
|
146
344
|
const getLoader = async () => {
|
|
147
345
|
if (disposed)
|
|
@@ -161,21 +359,46 @@ export const AntigravityV2Plugin = Plugin.define({
|
|
|
161
359
|
// Finish an older manager's pending save before selecting a new
|
|
162
360
|
// connection, so it cannot overwrite the newly selected cursors.
|
|
163
361
|
await transport.flush?.();
|
|
362
|
+
if (generation !== expectedGeneration)
|
|
363
|
+
return undefined;
|
|
164
364
|
const token = parseRefreshParts(auth.refresh).refreshToken;
|
|
165
365
|
let stored = await loadAccounts();
|
|
366
|
+
if (generation !== expectedGeneration)
|
|
367
|
+
return undefined;
|
|
166
368
|
if (!stored?.accounts.some((account) => account.refreshToken === token)) {
|
|
369
|
+
// A resolved pool member can disappear or rotate during I/O. Only a
|
|
370
|
+
// previously unknown native credential may add a new pool member.
|
|
371
|
+
if (resolved.accountID)
|
|
372
|
+
return undefined;
|
|
373
|
+
const latest = await resolveAuth();
|
|
374
|
+
if (generation !== expectedGeneration || latest?.identity !== identity)
|
|
375
|
+
return undefined;
|
|
167
376
|
await rememberAuth(auth);
|
|
168
|
-
|
|
377
|
+
return undefined;
|
|
169
378
|
}
|
|
170
379
|
if (resolved.select && stored) {
|
|
171
380
|
const index = stored.accounts.findIndex((account) => account.refreshToken === token && account.enabled !== false);
|
|
172
381
|
if (index >= 0) {
|
|
173
382
|
stored.activeIndex = index;
|
|
174
383
|
stored.activeIndexByFamily = { claude: index, gemini: index };
|
|
175
|
-
await
|
|
384
|
+
await updateAccounts(latest => {
|
|
385
|
+
const selected = latest.accounts.findIndex(account => account.refreshToken === token && account.enabled !== false);
|
|
386
|
+
if (selected >= 0) {
|
|
387
|
+
latest.activeIndex = selected;
|
|
388
|
+
latest.activeIndexByFamily = { claude: selected, gemini: selected };
|
|
389
|
+
}
|
|
390
|
+
});
|
|
176
391
|
}
|
|
177
392
|
}
|
|
178
|
-
|
|
393
|
+
let result;
|
|
394
|
+
try {
|
|
395
|
+
result = await transport.auth.loader(async () => await currentAuth() ?? { type: "none" }, {});
|
|
396
|
+
}
|
|
397
|
+
catch (error) {
|
|
398
|
+
if (!disposed && generation !== expectedGeneration)
|
|
399
|
+
return undefined;
|
|
400
|
+
throw error;
|
|
401
|
+
}
|
|
179
402
|
if (typeof result.fetch !== "function")
|
|
180
403
|
throw new Error("Antigravity OAuth is unavailable");
|
|
181
404
|
if (disposed)
|
|
@@ -195,31 +418,40 @@ export const AntigravityV2Plugin = Plugin.define({
|
|
|
195
418
|
}
|
|
196
419
|
return result ?? getLoader();
|
|
197
420
|
};
|
|
198
|
-
const scopedFetch = (fallback, body) =>
|
|
421
|
+
const scopedFetch = (fallback, body) => (input, init) => runCredentialOperation(async () => {
|
|
199
422
|
if (disposed)
|
|
200
423
|
throw new Error("Antigravity plugin has been disposed");
|
|
201
424
|
const original = typeof fallback === "function" ? fallback : globalThis.fetch;
|
|
202
425
|
if (!await currentAuth())
|
|
203
|
-
|
|
426
|
+
throw new Error("Connect Google with Antigravity OAuth and enable an account to use this model");
|
|
204
427
|
if (body && typeof init?.body === "string") {
|
|
205
428
|
init = { ...init, body: JSON.stringify(mergeBody(JSON.parse(init.body), body)) };
|
|
206
429
|
}
|
|
207
430
|
const interceptor = await getLoader();
|
|
208
431
|
const signal = AbortSignal.any([
|
|
209
432
|
lifetime.signal,
|
|
433
|
+
requestLifetime.signal,
|
|
210
434
|
...(init?.signal ? [init.signal] : input instanceof Request ? [input.signal] : []),
|
|
211
435
|
]);
|
|
212
436
|
return outbound.run(original, () => interceptor.fetch(input instanceof URL ? input.toString() : input, { ...init, signal }));
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
437
|
+
});
|
|
438
|
+
let cleaning;
|
|
439
|
+
const cleanup = () => {
|
|
440
|
+
if (cleaning)
|
|
441
|
+
return cleaning;
|
|
217
442
|
disposed = true;
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
443
|
+
cleaning = Promise.resolve().then(async () => {
|
|
444
|
+
lifetime.abort(new Error("Antigravity plugin disposed"));
|
|
445
|
+
await Promise.allSettled([...listeners].map((listener) => listener.close()));
|
|
446
|
+
await Promise.allSettled([...credentialOperations]);
|
|
447
|
+
await connectionEvents;
|
|
448
|
+
await Promise.allSettled([...discoveries]);
|
|
449
|
+
await Promise.allSettled([...catalogWrites]);
|
|
450
|
+
await Promise.allSettled(registrations.reverse().map((registration) => registration.dispose()));
|
|
451
|
+
await transport.dispose?.();
|
|
452
|
+
loader = undefined;
|
|
453
|
+
});
|
|
454
|
+
return cleaning;
|
|
223
455
|
};
|
|
224
456
|
try {
|
|
225
457
|
registrations.push(await ctx.integration.transform((editor) => {
|
|
@@ -235,76 +467,114 @@ export const AntigravityV2Plugin = Plugin.define({
|
|
|
235
467
|
],
|
|
236
468
|
},
|
|
237
469
|
async authorize(answer) {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
if (
|
|
248
|
-
|
|
470
|
+
return runCredentialOperation(async () => {
|
|
471
|
+
const auth = await authorizeAntigravity(typeof answer.projectId === "string" ? answer.projectId : "");
|
|
472
|
+
if (disposed)
|
|
473
|
+
throw new Error("Antigravity plugin has been disposed");
|
|
474
|
+
const state = new URL(auth.url).searchParams.get("state");
|
|
475
|
+
if (!state)
|
|
476
|
+
throw new Error("Antigravity authorization did not provide OAuth state");
|
|
477
|
+
const complete = (input) => runCredentialOperation(async () => {
|
|
478
|
+
let code = input.trim();
|
|
479
|
+
if (/^https?:\/\//i.test(code)) {
|
|
480
|
+
const callback = new URL(code);
|
|
481
|
+
const redirect = new URL(ANTIGRAVITY_REDIRECT_URI);
|
|
482
|
+
if (callback.origin !== redirect.origin || callback.pathname !== redirect.pathname || callback.searchParams.get("state") !== state) {
|
|
483
|
+
throw new Error("Invalid Antigravity OAuth callback URL or state");
|
|
484
|
+
}
|
|
485
|
+
code = callback.searchParams.get("code") ?? "";
|
|
249
486
|
}
|
|
250
|
-
|
|
487
|
+
if (!code)
|
|
488
|
+
throw new Error("Missing Antigravity authorization code");
|
|
489
|
+
const result = await exchangeAntigravity(code, state);
|
|
490
|
+
if (result.type !== "success")
|
|
491
|
+
throw new Error("Antigravity authorization failed; please try signing in again");
|
|
492
|
+
const account = await rememberAuth({ ...result, type: "oauth" }, result.email);
|
|
493
|
+
await updateAccounts(accounts => {
|
|
494
|
+
const current = accounts.accounts.find(item => item.id === account?.id);
|
|
495
|
+
if (!current)
|
|
496
|
+
return;
|
|
497
|
+
current.enabled = true;
|
|
498
|
+
current.verificationRequired = false;
|
|
499
|
+
delete current.verificationRequiredAt;
|
|
500
|
+
delete current.verificationRequiredReason;
|
|
501
|
+
delete current.verificationUrl;
|
|
502
|
+
});
|
|
503
|
+
await ctx.storage.remove("pool-selection");
|
|
504
|
+
invalidateLoader();
|
|
505
|
+
return oauthCredential({ ...result, type: "oauth" }, { ...(result.email ? { email: result.email } : {}), ...(account?.id ? { antigravityAccountId: account.id } : {}) });
|
|
506
|
+
});
|
|
507
|
+
if (answer.manual !== false) {
|
|
508
|
+
return { url: auth.url, instructions: "Open this URL, then paste the authorization code or full callback URL.", mode: "code", callback: complete };
|
|
509
|
+
}
|
|
510
|
+
const listener = await startOAuthListener();
|
|
511
|
+
listeners.add(listener);
|
|
512
|
+
const callback = listener.waitForCallback()
|
|
513
|
+
.then((url) => complete(url.toString()))
|
|
514
|
+
.finally(async () => {
|
|
515
|
+
listeners.delete(listener);
|
|
516
|
+
await listener.close();
|
|
517
|
+
});
|
|
518
|
+
// The host consumes this promise after returning the authorization.
|
|
519
|
+
void callback.catch(() => undefined);
|
|
520
|
+
if (disposed) {
|
|
521
|
+
await listener.close();
|
|
522
|
+
throw new Error("Antigravity plugin has been disposed");
|
|
251
523
|
}
|
|
252
|
-
|
|
253
|
-
throw new Error("Missing Antigravity authorization code");
|
|
254
|
-
const result = await exchangeAntigravity(code, state);
|
|
255
|
-
if (result.type !== "success")
|
|
256
|
-
throw new Error("Antigravity authorization failed; please try signing in again");
|
|
257
|
-
await persistAccountPool([result]);
|
|
258
|
-
invalidateLoader();
|
|
259
|
-
return oauthCredential({ ...result, type: "oauth" }, result.email ? { email: result.email } : undefined);
|
|
260
|
-
};
|
|
261
|
-
if (answer.manual !== false) {
|
|
262
|
-
return { url: auth.url, instructions: "Open this URL, then paste the authorization code or full callback URL.", mode: "code", callback: complete };
|
|
263
|
-
}
|
|
264
|
-
const listener = await startOAuthListener();
|
|
265
|
-
listeners.add(listener);
|
|
266
|
-
const callback = listener.waitForCallback()
|
|
267
|
-
.then((url) => complete(url.toString()))
|
|
268
|
-
.finally(async () => {
|
|
269
|
-
listeners.delete(listener);
|
|
270
|
-
await listener.close();
|
|
524
|
+
return { url: auth.url, instructions: "Open this URL in your browser to connect Antigravity.", mode: "auto", callback };
|
|
271
525
|
});
|
|
272
|
-
// The host consumes this promise after returning the authorization.
|
|
273
|
-
void callback.catch(() => undefined);
|
|
274
|
-
return { url: auth.url, instructions: "Open this URL in your browser to connect Antigravity.", mode: "auto", callback };
|
|
275
526
|
},
|
|
276
527
|
async refresh(credential) {
|
|
277
|
-
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
528
|
+
return runCredentialOperation(async () => {
|
|
529
|
+
const sourceConnection = await ctx.integration.connection.active(PROVIDER_ID);
|
|
530
|
+
const stored = await loadManagedAccounts();
|
|
531
|
+
const original = stored?.accounts.find(account => matchesAccountToken(account, parseRefreshParts(credential.refresh).refreshToken) || (typeof credential.metadata?.antigravityAccountId === "string" && account.id === credential.metadata.antigravityAccountId));
|
|
532
|
+
const previous = original?.id ? { ...credential, refresh: formatRefreshParts(original), metadata: { ...credential.metadata, antigravityAccountId: original.id } } : credential;
|
|
533
|
+
try {
|
|
534
|
+
const refreshed = await refreshAccessToken(previous, client, PROVIDER_ID);
|
|
535
|
+
if (!refreshed?.access)
|
|
536
|
+
throw new Error("Antigravity token refresh temporarily unavailable");
|
|
537
|
+
const account = await rememberAuth(refreshed, original?.email, previous);
|
|
538
|
+
invalidateLoader(false);
|
|
539
|
+
return oauthCredential(account ? { ...refreshed, refresh: formatRefreshParts(account) } : refreshed, { ...credential.metadata, ...(account?.id ? { antigravityAccountId: account.id } : {}) });
|
|
540
|
+
}
|
|
541
|
+
catch (error) {
|
|
542
|
+
if (error instanceof AntigravityTokenRefreshError && error.code === "invalid_grant") {
|
|
543
|
+
let revoked = false;
|
|
544
|
+
await updateAccounts(stored => {
|
|
545
|
+
const account = stored.accounts.find(item => item.refreshToken === parseRefreshParts(previous.refresh).refreshToken);
|
|
546
|
+
if (account) {
|
|
547
|
+
account.enabled = false;
|
|
548
|
+
revoked = true;
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
const boundID = sourceConnection?.type === "credential" ? await ctx.storage.get(`connection-account:${sourceConnection.id}`) : undefined;
|
|
552
|
+
if (revoked && sourceConnection && typeof boundID === "string" && boundID === original?.id)
|
|
553
|
+
await ctx.integration.connection.status({ integrationID: PROVIDER_ID, connection: sourceConnection, status: { status: "needs_auth", message: "Google revoked this account; sign in again." } });
|
|
554
|
+
invalidateLoader();
|
|
293
555
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
556
|
+
// Core resolves credentials before model construction. Keep that
|
|
557
|
+
// step available so the transport can rotate to another account.
|
|
558
|
+
// Six minutes clears Core's five-minute refresh buffer briefly.
|
|
559
|
+
return { ...credential, access: "", expires: Date.now() + 6 * 60_000 };
|
|
298
560
|
}
|
|
299
|
-
|
|
300
|
-
// step available so the transport can rotate to another account.
|
|
301
|
-
// Six minutes clears Core's five-minute refresh buffer briefly.
|
|
302
|
-
return { ...credential, access: "", expires: Date.now() + 6 * 60_000 };
|
|
303
|
-
}
|
|
561
|
+
});
|
|
304
562
|
},
|
|
305
563
|
label: (credential) => typeof credential.metadata?.email === "string" ? credential.metadata.email : undefined,
|
|
306
564
|
});
|
|
307
565
|
}));
|
|
566
|
+
connectionEvents = (async () => {
|
|
567
|
+
for await (const event of ctx.event.subscribe({ signal: lifetime.signal })) {
|
|
568
|
+
if (event.type === "credential.updated" || event.type === "integration.updated"
|
|
569
|
+
|| (event.type === "credential.switched" && event.data.integrationID === PROVIDER_ID)) {
|
|
570
|
+
await refreshModelAccess(true);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
})().catch(error => {
|
|
574
|
+
if (!disposed)
|
|
575
|
+
log.warn("Google connection event stream stopped", { error: String(error) });
|
|
576
|
+
});
|
|
577
|
+
await refreshModelAccess(false);
|
|
308
578
|
registrations.push(await ctx.provider.transform((editor) => {
|
|
309
579
|
// Keep the native Google provider and every unrelated model intact.
|
|
310
580
|
// Only a missing provider (e.g. a minimal host) needs a source template.
|
|
@@ -320,7 +590,13 @@ export const AntigravityV2Plugin = Plugin.define({
|
|
|
320
590
|
provider.activation = "enabled";
|
|
321
591
|
});
|
|
322
592
|
}
|
|
323
|
-
|
|
593
|
+
if (accountCatalogLoaded) {
|
|
594
|
+
for (const id of editor.get(PROVIDER_ID)?.models.keys() ?? []) {
|
|
595
|
+
if (ownsModel(id) && !modelDefinitions[id])
|
|
596
|
+
editor.models.remove(PROVIDER_ID, id);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
for (const [id, definition] of Object.entries(modelDefinitions)) {
|
|
324
600
|
editor.models.update(PROVIDER_ID, id, (model) => {
|
|
325
601
|
Object.assign(model, structuredClone(definition), { package: GOOGLE_PACKAGE });
|
|
326
602
|
model.variants = structuredClone(definition.variants).map((variant) => ({ ...variant, id: Model.VariantID.make(variant.id) }));
|
|
@@ -330,8 +606,22 @@ export const AntigravityV2Plugin = Plugin.define({
|
|
|
330
606
|
registrations.push(await ctx.model.transform((editor) => {
|
|
331
607
|
for (const model of editor.list(PROVIDER_ID)) {
|
|
332
608
|
const id = String(model.id);
|
|
333
|
-
if (!ownsModel(id))
|
|
609
|
+
if (!ownsModel(id)) {
|
|
610
|
+
// Core uses OAuth access tokens as API keys on the native Google
|
|
611
|
+
// route. Only the Antigravity catalog can use this connection.
|
|
612
|
+
if (usesAntigravityOAuth)
|
|
613
|
+
editor.update(PROVIDER_ID, id, target => { target.enabled = false; });
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
if (accountCatalogLoaded && !modelDefinitions[id]) {
|
|
617
|
+
// Configured models are raw overrides. Removing an override can
|
|
618
|
+
// reveal its native Google route; disable it explicitly instead.
|
|
619
|
+
editor.update(PROVIDER_ID, id, target => {
|
|
620
|
+
target.enabled = false;
|
|
621
|
+
target.package = GOOGLE_PACKAGE;
|
|
622
|
+
});
|
|
334
623
|
continue;
|
|
624
|
+
}
|
|
335
625
|
editor.update(PROVIDER_ID, id, (target) => {
|
|
336
626
|
// A native provider override can otherwise bypass the SDK hook.
|
|
337
627
|
target.package = GOOGLE_PACKAGE;
|
|
@@ -339,7 +629,12 @@ export const AntigravityV2Plugin = Plugin.define({
|
|
|
339
629
|
}
|
|
340
630
|
}));
|
|
341
631
|
registrations.push(await ctx.aisdk.hook("sdk", async (event) => {
|
|
342
|
-
if (
|
|
632
|
+
if (!ownsModel(event.model.id))
|
|
633
|
+
return;
|
|
634
|
+
if (accountCatalogLoaded && !modelDefinitions[event.model.id]) {
|
|
635
|
+
throw new Error("This model is unavailable to the enabled Antigravity accounts. Refresh available models from /antigravity.");
|
|
636
|
+
}
|
|
637
|
+
if (event.package !== SDK_PACKAGE)
|
|
343
638
|
return;
|
|
344
639
|
event.sdk = createGoogleGenerativeAI({
|
|
345
640
|
...event.options,
|
|
@@ -365,21 +660,58 @@ export const AntigravityV2Plugin = Plugin.define({
|
|
|
365
660
|
additionalProperties: false,
|
|
366
661
|
},
|
|
367
662
|
async execute(input, context) {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
auth =
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
663
|
+
return runCredentialOperation(async () => {
|
|
664
|
+
const signal = AbortSignal.any([context.signal, lifetime.signal, requestLifetime.signal]);
|
|
665
|
+
signal.throwIfAborted();
|
|
666
|
+
const args = input;
|
|
667
|
+
if (!args || typeof args.query !== "string" || !args.query.trim())
|
|
668
|
+
throw new Error("A search query is required");
|
|
669
|
+
const resolved = await resolveAuth();
|
|
670
|
+
let auth = resolved?.auth;
|
|
671
|
+
if (!auth)
|
|
672
|
+
return { content: "Connect Google with Antigravity OAuth to use Google Search." };
|
|
673
|
+
const metadata = resolved?.accountID ? { antigravityAccountId: resolved.accountID } : undefined;
|
|
674
|
+
if (!auth.access || accessTokenExpired(auth)) {
|
|
675
|
+
const previous = oauthCredential(auth, metadata);
|
|
676
|
+
auth = await refreshAccessToken(auth, client, PROVIDER_ID);
|
|
677
|
+
if (auth)
|
|
678
|
+
await rememberAuth(auth, undefined, previous);
|
|
679
|
+
}
|
|
680
|
+
if (!auth?.access)
|
|
681
|
+
throw new Error("Antigravity token refresh failed; please sign in again");
|
|
682
|
+
signal.throwIfAborted();
|
|
683
|
+
const project = await ensureProjectContext(auth);
|
|
684
|
+
if (project.auth.refresh !== auth.refresh)
|
|
685
|
+
await rememberAuth(project.auth, undefined, oauthCredential(auth, metadata));
|
|
686
|
+
signal.throwIfAborted();
|
|
687
|
+
return { content: await executeSearch(args, auth.access, project.effectiveProjectId, signal) };
|
|
688
|
+
});
|
|
380
689
|
},
|
|
381
690
|
});
|
|
382
691
|
}));
|
|
692
|
+
registrations.push(await registerAccountManagement(ctx, client, async () => {
|
|
693
|
+
invalidateLoader();
|
|
694
|
+
await transport.flush?.();
|
|
695
|
+
}, refreshModels, async () => {
|
|
696
|
+
hasSavedAccounts = (await loadAccounts())?.accounts.some(account => account.enabled !== false);
|
|
697
|
+
if (hasSavedAccounts && ctx.options.discoverModels !== false) {
|
|
698
|
+
try {
|
|
699
|
+
await refreshModels();
|
|
700
|
+
}
|
|
701
|
+
catch (error) {
|
|
702
|
+
log.warn("Model refresh unavailable", { error: String(error) });
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
await ctx.provider.reload();
|
|
706
|
+
}, runCredentialOperation));
|
|
707
|
+
if (hasSavedAccounts && ctx.options.discoverModels !== false && (!validCache || Date.now() - (cached?.updatedAt ?? 0) > 3600_000)) {
|
|
708
|
+
try {
|
|
709
|
+
await refreshModels();
|
|
710
|
+
}
|
|
711
|
+
catch (error) {
|
|
712
|
+
log.warn("Model discovery unavailable; using cached or fallback models", { error: String(error) });
|
|
713
|
+
}
|
|
714
|
+
}
|
|
383
715
|
return cleanup;
|
|
384
716
|
}
|
|
385
717
|
catch (error) {
|