| author | |
| committer | |
| log | c8872fd22aadb8d155f1bedc10576d8314f93e56 |
| tree | 38be5358a6fe866b9e2ed8298d4f023219c70e83 |
| parent | add8c3a55ac8a7d520dc2ea11a8fa05bc3cee361 |
| signature | Signed by PGP key B5690EEEBB952194 |
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 | 264 | ), |
| 265 | 265 | ); |
| 266 | 266 | |
| 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 | 284 | it.effect("treats initialize as installation proof, not account or model discovery", () => |
| 268 | 285 | Effect.scoped( |
| 269 | 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 | 144 | installed: false, |
| 145 | 145 | version: null, |
| 146 | 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 | 150 | message: settings.enabled |
| 149 | 151 | ? "Checking Antigravity availability." |
| 150 | 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 | 1179 | }); |
| 1180 | 1180 | }); |
| 1181 | 1181 | |
| 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 | 1289 | it("fills missing capabilities from the previous provider snapshot", () => { |
| 1183 | 1290 | const previousProvider = { |
| 1184 | 1291 | instanceId: ProviderInstanceId.make("cursor"), |
apps/server/src/provider/Layers/ProviderRegistry.ts+60-22| ... | ... | @@ -162,31 +162,69 @@ const mergeProviderModels = ( |
| 162 | 162 | : mergedModels; |
| 163 | 163 | }; |
| 164 | 164 | |
| 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 | */ | |
| 173 | const 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 | ||
| 165 | 197 | export const mergeProviderSnapshot = ( |
| 166 | 198 | previousProvider: ServerProvider | undefined, |
| 167 | 199 | nextProvider: ServerProvider, |
| 168 | ): ServerProvider => | |
| 169 | !previousProvider | |
| 170 | ? nextProvider | |
| 171 | : { | |
| 172 | ...nextProvider, | |
| 173 | models: mergeProviderModels(nextProvider, previousProvider.models, nextProvider.models), | |
| 174 | ...(nextProvider.workspaceSnapshots !== undefined | |
| 175 | ? { workspaceSnapshots: nextProvider.workspaceSnapshots } | |
| 176 | : previousProvider.workspaceSnapshots !== undefined | |
| 177 | ? { workspaceSnapshots: previousProvider.workspaceSnapshots } | |
| 178 | : {}), | |
| 179 | ...(shouldRetainMissingOpenCodeMetadata(nextProvider) | |
| 180 | ? { | |
| 181 | slashCommands: | |
| 182 | nextProvider.slashCommands.length === 0 | |
| 183 | ? previousProvider.slashCommands | |
| 184 | : nextProvider.slashCommands, | |
| 185 | skills: | |
| 186 | nextProvider.skills.length === 0 ? previousProvider.skills : nextProvider.skills, | |
| 187 | } | |
| 188 | : {}), | |
| 189 | }; | |
| 200 | ): ServerProvider => { | |
| 201 | if (!previousProvider) { | |
| 202 | return nextProvider; | |
| 203 | } | |
| 204 | const savedAccount = carrySavedAntigravityAccount(previousProvider, nextProvider); | |
| 205 | // "Google account access is not checked yet" describes the probe, not the | |
| 206 | // account; it must not outlive the state it explained. | |
| 207 | const { message: _uncheckedMessage, ...nextWithoutMessage } = nextProvider; | |
| 208 | return { | |
| 209 | ...(savedAccount?.status === "ready" ? nextWithoutMessage : nextProvider), | |
| 210 | ...savedAccount, | |
| 211 | models: mergeProviderModels(nextProvider, previousProvider.models, nextProvider.models), | |
| 212 | ...(nextProvider.workspaceSnapshots !== undefined | |
| 213 | ? { workspaceSnapshots: nextProvider.workspaceSnapshots } | |
| 214 | : previousProvider.workspaceSnapshots !== undefined | |
| 215 | ? { workspaceSnapshots: previousProvider.workspaceSnapshots } | |
| 216 | : {}), | |
| 217 | ...(shouldRetainMissingOpenCodeMetadata(nextProvider) | |
| 218 | ? { | |
| 219 | slashCommands: | |
| 220 | nextProvider.slashCommands.length === 0 | |
| 221 | ? previousProvider.slashCommands | |
| 222 | : nextProvider.slashCommands, | |
| 223 | skills: nextProvider.skills.length === 0 ? previousProvider.skills : nextProvider.skills, | |
| 224 | } | |
| 225 | : {}), | |
| 226 | }; | |
| 227 | }; | |
| 190 | 228 | |
| 191 | 229 | const haveProvidersChanged = ( |
| 192 | 230 | previousProviders: ReadonlyArray<ServerProvider>, |
docs/user/providers-antigravity.md+3| ... | ... | @@ -123,6 +123,9 @@ is refused while the runtime is in use. |
| 123 | 123 | |
| 124 | 124 | ## Check access and troubleshoot |
| 125 | 125 | |
| 126 | A server restart keeps your Google sign-in. The provider shows the saved account | |
| 127 | until a session, a refresh, or a sign-out reports something new. | |
| 128 | ||
| 126 | 129 | To check access and reload models, use **Refresh provider status** in web or desktop |
| 127 | 130 | provider settings, or **Refresh models** in mobile thread settings. If asked to |
| 128 | 131 | sign in again, use setup on web or desktop. |