authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-09-05 20:54:29-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-05 20:54:29-07:00
logc8872fd22aadb8d155f1bedc10576d8314f93e56
tree38be5358a6fe866b9e2ed8298d4f023219c70e83
parentadd8c3a55ac8a7d520dc2ea11a8fa05bc3cee361
signature Signed by PGP key B5690EEEBB952194

fix(server): keep the Antigravity Google sign-in across server restarts (#10244)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

5 files changed, 190 insertions(+), 23 deletions(-)

apps/server/src/provider/Layers/AntigravityProvider.test.ts+17
...@@ -264,6 +264,23 @@ it.layer(testLayer)("Antigravity provider snapshots", (it) => {...@@ -264,6 +264,23 @@ it.layer(testLayer)("Antigravity provider snapshots", (it) => {
264 ),264 ),
265 );265 );
266266
267 it.effect("publishes the configured sign-in method before any account is checked", () =>
268 Effect.scoped(
269 Effect.gen(function* () {
270 const provider = yield* makeAntigravityProvider(decodeSettings({ enabled: true }), {
271 stampIdentity: (snapshot) => Effect.succeed({ ...snapshot, instanceId, driver }),
272 probe: Effect.succeed(initializeResult),
273 supportsTextGeneration: Effect.succeed(true),
274 auth: { type: "gemini-api-key", label: "Gemini API key" },
275 });
276 expect((yield* provider.snapshot.getSnapshot).auth).toEqual({
277 status: "unknown",
278 type: "gemini-api-key",
279 });
280 }),
281 ),
282 );
283
267 it.effect("treats initialize as installation proof, not account or model discovery", () =>284 it.effect("treats initialize as installation proof, not account or model discovery", () =>
268 Effect.scoped(285 Effect.scoped(
269 Effect.gen(function* () {286 Effect.gen(function* () {
apps/server/src/provider/Layers/AntigravityProvider.ts+3-1
...@@ -144,7 +144,9 @@ export const makeAntigravityProvider = Effect.fn("makeAntigravityProvider")(func...@@ -144,7 +144,9 @@ export const makeAntigravityProvider = Effect.fn("makeAntigravityProvider")(func
144 installed: false,144 installed: false,
145 version: null,145 version: null,
146 status: "warning",146 status: "warning",
147 auth: { status: "unknown" },147 // The configured method rides along so the registry can tell a saved
148 // account for this method from one left by a previous configuration.
149 auth: { status: "unknown", ...(options.auth ? { type: options.auth.type } : {}) },
148 message: settings.enabled150 message: settings.enabled
149 ? "Checking Antigravity availability."151 ? "Checking Antigravity availability."
150 : "Antigravity is disabled in T3 Code settings.",152 : "Antigravity is disabled in T3 Code settings.",
apps/server/src/provider/Layers/ProviderRegistry.test.ts+107
...@@ -1179,6 +1179,113 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te...@@ -1179,6 +1179,113 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te
1179 });1179 });
1180 });1180 });
11811181
1182 describe("Antigravity saved account", () => {
1183 const signedIn = {
1184 instanceId: ProviderInstanceId.make("antigravity-personal"),
1185 driver: ProviderDriverKind.make("antigravity"),
1186 status: "ready",
1187 enabled: true,
1188 installed: true,
1189 auth: { status: "authenticated", type: "oauth-personal", label: "Google account" },
1190 checkedAt: "2026-09-05T00:00:00.000Z",
1191 version: "agy_acp_server_1.1.1",
1192 models: [
1193 {
1194 slug: "gemini-3.7-flash-high",
1195 name: "Gemini 3.7 Flash",
1196 isCustom: false,
1197 capabilities: null,
1198 },
1199 ],
1200 slashCommands: [{ name: "plan" }],
1201 skills: [],
1202 } as const satisfies ServerProvider;
1203 const uncheckedMessage =
1204 "Antigravity is installed. Google account access is not checked yet.";
1205 const restartProbe = {
1206 ...signedIn,
1207 status: "warning",
1208 auth: { status: "unknown" },
1209 checkedAt: "2026-09-05T00:01:00.000Z",
1210 message: uncheckedMessage,
1211 models: [],
1212 } as const satisfies ServerProvider;
1213
1214 it("keeps the saved Google account through restart health checks", () => {
1215 const merged = mergeProviderSnapshot(signedIn, restartProbe);
1216 const { message: _uncheckedMessage, ...probeWithoutMessage } = restartProbe;
1217 assert.deepStrictEqual(merged, {
1218 ...probeWithoutMessage,
1219 status: "ready",
1220 auth: signedIn.auth,
1221 models: signedIn.models,
1222 });
1223 assert.equal("message" in merged, false);
1224 // The next periodic probe reads the merged snapshot as its previous state.
1225 assert.deepStrictEqual(mergeProviderSnapshot(merged, restartProbe), merged);
1226 });
1227
1228 it("carries the account through the boot probe and a failed probe without hiding them", () => {
1229 const booting = {
1230 ...restartProbe,
1231 installed: false,
1232 version: null,
1233 message: "Checking Antigravity availability.",
1234 } satisfies ServerProvider;
1235 assert.deepStrictEqual(mergeProviderSnapshot(signedIn, booting), {
1236 ...booting,
1237 auth: signedIn.auth,
1238 models: signedIn.models,
1239 });
1240
1241 const failed = {
1242 ...restartProbe,
1243 status: "error",
1244 message: "Antigravity did not respond to its local health check within 90 seconds.",
1245 } satisfies ServerProvider;
1246 assert.deepStrictEqual(mergeProviderSnapshot(signedIn, failed), {
1247 ...failed,
1248 auth: signedIn.auth,
1249 models: signedIn.models,
1250 });
1251 });
1252
1253 it("does not invent an account after sign-out, disable, uninstall, or for other providers", () => {
1254 const untouched = [
1255 { ...restartProbe, auth: { status: "unauthenticated" } },
1256 { ...restartProbe, status: "disabled", enabled: false },
1257 { ...restartProbe, status: "error", installed: false },
1258 { ...restartProbe, driver: ProviderDriverKind.make("codex") },
1259 // The instance was rebuilt with another sign-in method.
1260 { ...restartProbe, auth: { status: "unknown", type: "gemini-api-key" } },
1261 ] satisfies ReadonlyArray<ServerProvider>;
1262 for (const next of untouched) {
1263 const merged = mergeProviderSnapshot(signedIn, next);
1264 assert.deepStrictEqual(merged.auth, next.auth);
1265 assert.equal(merged.status, next.status);
1266 assert.equal(merged.message, next.message);
1267 }
1268 assert.deepStrictEqual(
1269 mergeProviderSnapshot({ ...signedIn, auth: { status: "unknown" } }, restartProbe).auth,
1270 { status: "unknown" },
1271 );
1272 assert.equal(
1273 mergeProviderSnapshot(
1274 { ...signedIn, driver: ProviderDriverKind.make("codex") },
1275 restartProbe,
1276 ).auth.status,
1277 "unknown",
1278 );
1279 assert.deepStrictEqual(
1280 mergeProviderSnapshot(signedIn, {
1281 ...restartProbe,
1282 auth: { status: "unknown", type: "oauth-personal" },
1283 }).auth,
1284 signedIn.auth,
1285 );
1286 });
1287 });
1288
1182 it("fills missing capabilities from the previous provider snapshot", () => {1289 it("fills missing capabilities from the previous provider snapshot", () => {
1183 const previousProvider = {1290 const previousProvider = {
1184 instanceId: ProviderInstanceId.make("cursor"),1291 instanceId: ProviderInstanceId.make("cursor"),
apps/server/src/provider/Layers/ProviderRegistry.ts+60-22
...@@ -162,31 +162,69 @@ const mergeProviderModels = (...@@ -162,31 +162,69 @@ const mergeProviderModels = (
162 : mergedModels;162 : mergedModels;
163};163};
164164
165/**
166 * Antigravity's health check only initializes the agent, so after a server
167 * restart it reports the account as unchecked. The saved Google login still
168 * works, and the previous snapshot proves it. Carry that account state until
169 * a session, refresh, or sign-out reports something new. A confirmed missing
170 * installation, sign-out, disabled instance, or a changed sign-in method is
171 * never overridden.
172 */
173const carrySavedAntigravityAccount = (
174 previousProvider: ServerProvider,
175 nextProvider: ServerProvider,
176): Pick<ServerProvider, "auth" | "status"> | undefined => {
177 const antigravity = ProviderDriverKind.make("antigravity");
178 if (
179 nextProvider.driver !== antigravity ||
180 previousProvider.driver !== antigravity ||
181 !nextProvider.enabled ||
182 nextProvider.auth.status !== "unknown" ||
183 previousProvider.auth.status !== "authenticated" ||
184 (nextProvider.auth.type !== undefined &&
185 nextProvider.auth.type !== previousProvider.auth.type) ||
186 (!nextProvider.installed && nextProvider.status !== "warning")
187 ) {
188 return undefined;
189 }
190 // The pending boot probe (`installed: false`, warning) and a failed probe
191 // keep their own status; only a passed health check reads as ready.
192 const status =
193 nextProvider.installed && nextProvider.status === "warning" ? "ready" : nextProvider.status;
194 return { auth: previousProvider.auth, status };
195};
196
165export const mergeProviderSnapshot = (197export const mergeProviderSnapshot = (
166 previousProvider: ServerProvider | undefined,198 previousProvider: ServerProvider | undefined,
167 nextProvider: ServerProvider,199 nextProvider: ServerProvider,
168): ServerProvider =>200): ServerProvider => {
169 !previousProvider201 if (!previousProvider) {
170 ? nextProvider202 return nextProvider;
171 : {203 }
172 ...nextProvider,204 const savedAccount = carrySavedAntigravityAccount(previousProvider, nextProvider);
173 models: mergeProviderModels(nextProvider, previousProvider.models, nextProvider.models),205 // "Google account access is not checked yet" describes the probe, not the
174 ...(nextProvider.workspaceSnapshots !== undefined206 // account; it must not outlive the state it explained.
175 ? { workspaceSnapshots: nextProvider.workspaceSnapshots }207 const { message: _uncheckedMessage, ...nextWithoutMessage } = nextProvider;
176 : previousProvider.workspaceSnapshots !== undefined208 return {
177 ? { workspaceSnapshots: previousProvider.workspaceSnapshots }209 ...(savedAccount?.status === "ready" ? nextWithoutMessage : nextProvider),
178 : {}),210 ...savedAccount,
179 ...(shouldRetainMissingOpenCodeMetadata(nextProvider)211 models: mergeProviderModels(nextProvider, previousProvider.models, nextProvider.models),
180 ? {212 ...(nextProvider.workspaceSnapshots !== undefined
181 slashCommands:213 ? { workspaceSnapshots: nextProvider.workspaceSnapshots }
182 nextProvider.slashCommands.length === 0214 : previousProvider.workspaceSnapshots !== undefined
183 ? previousProvider.slashCommands215 ? { workspaceSnapshots: previousProvider.workspaceSnapshots }
184 : nextProvider.slashCommands,216 : {}),
185 skills:217 ...(shouldRetainMissingOpenCodeMetadata(nextProvider)
186 nextProvider.skills.length === 0 ? previousProvider.skills : nextProvider.skills,218 ? {
187 }219 slashCommands:
188 : {}),220 nextProvider.slashCommands.length === 0
189 };221 ? previousProvider.slashCommands
222 : nextProvider.slashCommands,
223 skills: nextProvider.skills.length === 0 ? previousProvider.skills : nextProvider.skills,
224 }
225 : {}),
226 };
227};
190228
191const haveProvidersChanged = (229const haveProvidersChanged = (
192 previousProviders: ReadonlyArray<ServerProvider>,230 previousProviders: ReadonlyArray<ServerProvider>,
docs/user/providers-antigravity.md+3
...@@ -123,6 +123,9 @@ is refused while the runtime is in use....@@ -123,6 +123,9 @@ is refused while the runtime is in use.
123123
124## Check access and troubleshoot124## Check access and troubleshoot
125125
126A server restart keeps your Google sign-in. The provider shows the saved account
127until a session, a refresh, or a sign-out reports something new.
128
126To check access and reload models, use **Refresh provider status** in web or desktop129To check access and reload models, use **Refresh provider status** in web or desktop
127provider settings, or **Refresh models** in mobile thread settings. If asked to130provider settings, or **Refresh models** in mobile thread settings. If asked to
128sign in again, use setup on web or desktop.131sign in again, use setup on web or desktop.