# Files changed · PR \#21085 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/21085/diffs)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

[fix\(server\): Antigravity Change account opens Google sign-in again](https://git.cafe/pingdotgg/t3code/pull/21085.md)

Version: 8

Head commit: 77e0962918907476c155f586154b4a4c0cacc81c

Base commit: 9077e818d836ec6e9cc373d7469034cccc593a5a

Comparison base: 9077e818d836ec6e9cc373d7469034cccc593a5a

Showing up to 20 entries from this revision. If the pull request changes, reopen the first page before continuing.

## apps/server/src/provider/AntigravityAuth.test.ts

Status: modified; +260; −5

```diff
@@ -5,7 +5,9 @@
 import * as Effect from "effect/Effect";
 import * as Exit from "effect/Exit";
 import * as Fiber from "effect/Fiber";
+import * as FileSystem from "effect/FileSystem";
 import * as Option from "effect/Option";
+import * as Path from "effect/Path";
 import * as Scope from "effect/Scope";
 import * as Stream from "effect/Stream";
 import * as TestClock from "effect/testing/TestClock";
@@ -17,6 +19,7 @@
   type AntigravityAuth,
   type AntigravityAuthRuntime,
 } from "./AntigravityAuth.ts";
+import * as AntigravityCredentials from "./AntigravityCredentials.ts";
 import type * as AcpSessionRuntime from "@t3tools/provider-acp/server/AcpSessionRuntime";
 
 const instanceId = ProviderInstanceId.make("antigravity-auth-test");
@@ -54,9 +57,12 @@
 const makeHarness = Effect.fn("makeAuthTestHarness")(function* (
   options: {
     readonly interactive?: boolean;
+    readonly usesBrowser?: boolean;
+    readonly profileDirectory?: string;
     readonly authorizationUrls?: ReadonlyArray<string>;
     readonly supportsLogout?: boolean;
     readonly beforeInitialize?: Effect.Effect<void>;
+    readonly credentialFileSystem?: FileSystem.FileSystem;
     readonly forwardCallback?: Effect.Effect<void, ProviderSetupError>;
   } = {},
 ) {
@@ -64,6 +70,31 @@
   const discovered = yield* Deferred.make<void, AcpErrors.AcpError>();
   const closed = yield* Deferred.make<void>();
   const events: string[] = [];
+  const fs = yield* FileSystem.FileSystem;
+  const path = yield* Path.Path;
+  const tokenPath = options.profileDirectory
+    ? path.join(options.profileDirectory, "antigravity-acp", "acp_token.json")
+    : undefined;
+  const readToken = (
+    tokenPath
+      ? fs
+          .exists(tokenPath)
+          .pipe(
+            Effect.flatMap((exists) =>
+              exists ? fs.readFileString(tokenPath) : Effect.succeed(null),
+            ),
+          )
+      : Effect.succeed(null)
+  ).pipe(Effect.orDie);
+  const credentials = options.profileDirectory
+    ? yield* AntigravityCredentials.AntigravityCredentials.pipe(
+        Effect.provide(AntigravityCredentials.layer(options.profileDirectory)),
+        Effect.provideService(FileSystem.FileSystem, options.credentialFileSystem ?? fs),
+      )
+    : undefined;
+  const setupFailure = (cause: AntigravityCredentials.AntigravityCredentialsError) =>
+    new ProviderSetupError({ instanceId, operation: "credentials", detail: cause.message, cause });
+  const tokensAtClose: Array<string | null> = [];
   let receiveAuthorizationUrl:
     | ((url: string) => Effect.Effect<void, AcpErrors.AcpError>)
     | undefined;
@@ -73,10 +104,14 @@
     instanceId,
     makeRuntime: (input) =>
       Effect.gen(function* () {
+        if (tokenPath && input.onAuthorizationUrl) {
+          assert.equal(yield* readToken, options.usesBrowser === false ? "previous-account" : null);
+        }
         receiveAuthorizationUrl = input.onAuthorizationUrl;
         events.push("process-open");
         yield* Effect.addFinalizer(() =>
           Effect.gen(function* () {
+            if (tokenPath) tokensAtClose.push(yield* readToken);
             events.push("process-close");
             yield* Deferred.succeed(closed, undefined);
           }),
@@ -104,8 +139,11 @@
               return started;
             }),
           request: (method) =>
+            Effect.gen(function* () {
-            Effect.sync(() => {
               events.push(method);
+              if (method === "logout" && tokenPath) {
+                yield* fs.remove(tokenPath, { force: true }).pipe(Effect.orDie);
+              }
               return {};
             }),
         } satisfies AntigravityAuthRuntime;
@@ -115,10 +153,28 @@
         catalog = ["gemini-test"];
         events.push("catalog-published");
       }),
+    onSignedOut: (credentials?.discardBackup ?? Effect.void).pipe(
+      Effect.mapError(setupFailure),
+      Effect.andThen(
+        Effect.sync(() => {
+          catalog = [];
+          events.push("catalog-cleared");
+        }),
+      ),
+    ),
+    ...(options.usesBrowser === undefined ? {} : { usesBrowser: options.usesBrowser }),
+    ...(credentials
-    onSignedOut: Effect.sync(() => {
-      catalog = [];
-      events.push("catalog-cleared");
-    }),
+      ? {
+          withFreshCredentials: (
+            task: Effect.Effect<void, AcpErrors.AcpError | ProviderSetupError>,
+          ) =>
+            credentials.withFreshSignIn(task).pipe(
+              Effect.catchTags({
+                AntigravityCredentialsError: (cause) => Effect.fail(setupFailure(cause)),
+              }),
+            ),
+        }
+      : {}),
     forwardCallback: () =>
       options.forwardCallback ??
       Effect.sync(() => {
@@ -131,6 +187,7 @@
     discovered,
     closed,
     events,
+    tokensAtClose,
     catalog: () => catalog,
     forwarded: () => forwarded,
     receiveAuthorizationUrl: (url: string) =>
@@ -282,6 +339,204 @@
     }),
   );
 
+  const makeCredentialProfile = Effect.gen(function* () {
+    const fs = yield* FileSystem.FileSystem;
+    const path = yield* Path.Path;
+    const profileDirectory = yield* fs.makeTempDirectoryScoped();
+    const directory = path.join(profileDirectory, "antigravity-acp");
+    yield* fs.makeDirectory(directory);
+    const tokenPath = path.join(directory, "acp_token.json");
+    yield* fs.writeFileString(tokenPath, "previous-account");
+    return { fs, profileDirectory, directory, tokenPath };
+  });
+
+  it.effect.each(["success", "cancel", "auth failure", "model failure"] as const)(
+    "changes Google accounts without losing credentials on %s",
+    (outcome) =>
+      Effect.gen(function* () {
+        const profile = yield* makeCredentialProfile;
+        const harness = yield* makeHarness({ profileDirectory: profile.profileDirectory });
+        const processScope = yield* Scope.make();
+        yield* Effect.addFinalizer(() => Scope.close(processScope, Exit.void));
+        yield* harness.auth
+          .withProcess(
+            Effect.gen(function* () {
+              assert.equal(
+                yield* profile.fs.readFileString(profile.tokenPath).pipe(Effect.orDie),
+                "previous-account",
+              );
+              harness.events.push("chat-close");
+              yield* Scope.close(processScope, Exit.void);
+            }),
+            Effect.void,
+          )
+          .pipe(Scope.provide(processScope));
+        const state = yield* harness.auth.controller.start(
+          owner,
+          Effect.sync(() => {
+            harness.events.push("sessions-stop");
+          }),
+        );
+        // A failed start must settle too, so an isolation regression fails instead of waiting forever.
+        const pending = yield* harness.auth.controller.subscribe(owner).pipe(
+          Stream.filter((state) => state.phase !== "starting"),
+          Stream.runHead,
+          Effect.map(Option.getOrThrow),
+        );
+        assert.equal(pending.phase, "waiting");
+        assert.equal(pending.authorizationUrl, authorizationUrl);
+        assert.deepEqual(harness.events, [
+          "sessions-stop",
+          "chat-close",
+          "process-open",
+          "authenticate",
+        ]);
+        assert.isFalse(yield* profile.fs.exists(profile.tokenPath));
+
+        const savedNewToken = outcome === "success" || outcome === "model failure";
+        if (savedNewToken) {
+          yield* profile.fs.writeFileString(profile.tokenPath, "chosen-account");
+          yield* Deferred.succeed(harness.authenticated, undefined);
+        }
+        if (outcome === "cancel") {
+          yield* harness.auth.controller.cancel(owner, state.flowId!);
+        } else {
+          if (outcome === "success") {
+            yield* Deferred.succeed(harness.discovered, undefined);
+          } else {
+            yield* Deferred.fail(
+              outcome === "auth failure" ? harness.authenticated : harness.discovered,
+              AcpErrors.AcpRequestError.internalError("Sign-in failed"),
+            );
+          }
+          yield* phase(harness.auth, outcome === "success" ? "succeeded" : "failed");
+        }
+
+        assert.deepEqual(harness.tokensAtClose, [savedNewToken ? "chosen-account" : null]);
+        assert.equal(
+          yield* profile.fs.readFileString(profile.tokenPath),
+          savedNewToken ? "chosen-account" : "previous-account",
+        );
+        assert.deepEqual(yield* profile.fs.readDirectory(profile.directory), ["acp_token.json"]);
+      }),
+  );
+
+  it.effect("reports failed cancellation when saved credentials cannot be restored", () =>
+    Effect.gen(function* () {
+      const profile = yield* makeCredentialProfile;
+      const backupPath = `${profile.tokenPath}.before-sign-in`;
+      let failRestore = false;
+      const restoring = yield* Deferred.make<void>();
+      const continueRestore = yield* Deferred.make<void>();
+      const credentialFileSystem = FileSystem.FileSystem.of({
+        ...profile.fs,
+        link: (from, to) =>
+          Effect.gen(function* () {
+            if (failRestore) {
+              yield* Deferred.succeed(restoring, undefined);
+              yield* Deferred.await(continueRestore);
+            }
+            yield* profile.fs.link(
+              from,
+              failRestore ? `${profile.directory}/missing/acp_token.json` : to,
+            );
+          }),
+      });
+      const harness = yield* makeHarness({
+        profileDirectory: profile.profileDirectory,
+        credentialFileSystem,
+      });
+      const state = yield* harness.auth.controller.start(owner);
+      yield* phase(harness.auth, "waiting");
+      failRestore = true;
+      const cancellation = yield* harness.auth.controller
+        .cancel(owner, state.flowId!)
+        .pipe(Effect.forkScoped);
+      yield* Deferred.await(restoring);
+      const stopping = yield* harness.auth.controller
+        .subscribe(owner)
+        .pipe(Stream.runHead, Effect.map(Option.getOrThrow));
+      assert.equal(stopping.phase, "verifying");
+      assert.isTrue(
+        Exit.isFailure(yield* harness.auth.controller.start(otherOwner).pipe(Effect.exit)),
+      );
+      assert.isTrue(
+        Exit.isFailure(
+          yield* harness.auth
+            .withProcess(Effect.void, Effect.void)
+            .pipe(Effect.scoped, Effect.exit),
+        ),
+      );
+      yield* Deferred.succeed(continueRestore, undefined);
+      const result = yield* Fiber.join(cancellation);
+      const subscribed = yield* harness.auth.controller
+        .subscribe(owner)
+        .pipe(Stream.runHead, Effect.map(Option.getOrThrow));
+
+      assert.isFalse(yield* profile.fs.exists(profile.tokenPath));
+      assert.equal(yield* profile.fs.readFileString(backupPath), "previous-account");
+      assert.deepEqual(
+        { result: result.phase, subscribed: subscribed.phase },
+        { result: "failed", subscribed: "failed" },
+      );
+    }),
+  );
+
+  it.effect.each([false, true])(
+    "retires the old account on sign-out and reports backup cleanup failure (%s)",
+    (failCleanup) =>
+      Effect.gen(function* () {
+        const profile = yield* makeCredentialProfile;
+        const backupPath = `${profile.tokenPath}.before-sign-in`;
+        yield* profile.fs.writeFileString(backupPath, "older-account");
+        const credentialFileSystem = FileSystem.FileSystem.of({
+          ...profile.fs,
+          remove: (file, options) =>
+            file === backupPath && failCleanup
+              ? profile.fs.remove(profile.directory) // A nonempty directory cannot be removed.
+              : profile.fs.remove(file, options),
+        });
+        const harness = yield* makeHarness({
+          profileDirectory: profile.profileDirectory,
+          credentialFileSystem,
+        });
+        const result = yield* harness.auth.controller.logout(Effect.void).pipe(Effect.exit);
+        assert.equal(Exit.isFailure(result), failCleanup);
+        const state = yield* phase(harness.auth, failCleanup ? "failed" : "idle");
+        assert.equal(
+          state.message,
+          failCleanup ? "Antigravity sign-out failed. Try again." : "Signed out of Google.",
+        );
+        assert.isFalse(yield* profile.fs.exists(profile.tokenPath));
+        assert.equal(yield* profile.fs.exists(backupPath), failCleanup);
+        assert.deepEqual(harness.catalog(), failCleanup ? ["previous-account-model"] : []);
+        if (!failCleanup) {
+          const recreated = yield* AntigravityCredentials.AntigravityCredentials.pipe(
+            Effect.provide(AntigravityCredentials.layer(profile.profileDirectory)),
+          );
+          yield* recreated.recover;
+          assert.isFalse(yield* profile.fs.exists(profile.tokenPath));
+        }
+      }),
+  );
+
+  it.effect("leaves saved credentials available for non-browser authentication", () =>
+    Effect.gen(function* () {
+      const profile = yield* makeCredentialProfile;
+      const harness = yield* makeHarness({
+        profileDirectory: profile.profileDirectory,
+        usesBrowser: false,
+        interactive: false,
+      });
+      yield* harness.auth.controller.start(owner);
+      yield* Deferred.succeed(harness.authenticated, undefined);
+      yield* Deferred.succeed(harness.discovered, undefined);
+      yield* phase(harness.auth, "succeeded");
+      assert.equal(yield* profile.fs.readFileString(profile.tokenPath), "previous-account");
+      assert.deepEqual(yield* profile.fs.readDirectory(profile.directory), ["acp_token.json"]);
+    }),
+  );
+
   it.effect("rejects mismatched callbacks without sending any HTTP request", () =>
     Effect.gen(function* () {
       const harness = yield* makeHarness();
```

## apps/server/src/provider/AntigravityAuth.ts

Status: modified; +95; −41

```diff
@@ -47,7 +47,8 @@
   state: ProviderAuthState;
   pending: AntigravityAuthorizationUrl | undefined;
   callbackSent: boolean;
+  fiber: Fiber.Fiber<void, AcpErrors.AcpError | ProviderSetupError> | undefined;
+  result: Exit.Exit<void, unknown> | undefined;
-  fiber: Fiber.Fiber<void> | undefined;
   forwarding: Fiber.Fiber<void, ProviderSetupError> | undefined;
 }
 
@@ -80,9 +81,12 @@
     result: AcpSessionRuntime.AcpSessionRuntimeStartResult,
     runtime: Runtime,
   ) => Effect.Effect<void>;
-  readonly onSignedOut: Effect.Effect<void>;
+  readonly onSignedOut: Effect.Effect<void, ProviderSetupError>;
   readonly forwardCallback?: (callback: URL) => Effect.Effect<void, ProviderSetupError>;
   readonly usesBrowser?: boolean;
+  readonly withFreshCredentials?: (
+    task: Effect.Effect<void, AcpErrors.AcpError | ProviderSetupError>,
+  ) => Effect.Effect<void, AcpErrors.AcpError | ProviderSetupError>;
 }
 
 function visibleSnapshot(snapshot: AuthSnapshot, ownerSessionId: string): ProviderAuthState {
@@ -217,7 +221,7 @@
   const finishFlow = (flow: AuthFlow, result: Exit.Exit<void, unknown>) =>
     lock.withPermits(1)(
       Effect.gen(function* () {
+        if (activeFlow !== flow || operation !== "auth") return;
-        if (activeFlow !== flow) return;
         activeFlow = undefined;
         operation = "idle";
         flow.pending = undefined;
@@ -264,59 +268,92 @@
   const runSignIn = (flow: AuthFlow, stopSessions: Effect.Effect<void, ProviderSetupError>) =>
     Effect.gen(function* () {
       yield* stopSessions.pipe(Effect.ensuring(stopOwnedProcesses));
-      const runtime = yield* options.makeRuntime({
-        onAuthorizationUrl: (url) => receiveAuthorizationUrl(flow, url),
-      });
-      const started = yield* runtime.start();
-      yield* lock.withPermits(1)(
-        Effect.gen(function* () {
-          if (activeFlow !== flow) return;
+      const authenticate = Effect.gen(function* () {
+        const runtime = yield* options.makeRuntime({
+          onAuthorizationUrl: (url) => receiveAuthorizationUrl(flow, url),
+        });
+        const started = yield* runtime.start();
-          flow.pending = undefined;
-          yield* publishFlow(flow, {
+        yield* lock.withPermits(1)(
+          Effect.gen(function* () {
-            ...flow.state,
-            phase: "verifying",
-            authorizationUrl: null,
-            message: "Checking Antigravity access and models.",
-          });
-        }),
-      );
-      yield* options.onAuthenticated(started, runtime);
+            if (activeFlow !== flow || operation !== "auth") return;
+            flow.pending = undefined;
+            yield* publishFlow(flow, {
+              ...flow.state,
+              phase: "verifying",
+              authorizationUrl: null,
+              message: "Checking Antigravity access and models.",
+            });
+          }),
+        );
+        yield* options.onAuthenticated(started, runtime);
+      }).pipe(Effect.scoped);
+      const withFreshCredentials = options.withFreshCredentials;
+      if (usesBrowser && withFreshCredentials) {
+        yield* Effect.uninterruptibleMask((restore) =>
+          Effect.gen(function* () {
+            // Capture cleanup failure before pending interruption crosses the credential boundary.
+            const result = yield* withFreshCredentials(restore(authenticate)).pipe(Effect.exit);
+            flow.result = result;
+            return yield* result;
+          }),
+        );
+      } else {
+        yield* authenticate;
+      }
     }).pipe(
-      Effect.scoped,
       Effect.timeoutOrElse({
         duration: AUTH_TIMEOUT_MS,
         orElse: () =>
           Effect.fail(setupError("start", "Google sign-in expired. Start sign-in again.")),
       }),
-      Effect.exit,
-      Effect.flatMap((result) => finishFlow(flow, result)),
+      Effect.onExit((result) => {
+        const credentials = flow.result;
+        flow.result =
+          credentials && Exit.isFailure(credentials) && !Cause.hasInterruptsOnly(credentials.cause)
+            ? credentials
+            : result;
+        return finishFlow(flow, flow.result);
+      }),
     );
 
+  const interruptFlow = Effect.fn("AntigravityAuth.interruptFlow")(function* (flow: AuthFlow) {
+    if (flow.forwarding) yield* Fiber.interrupt(flow.forwarding);
+    if (!flow.fiber) return Exit.void;
+    yield* Fiber.interrupt(flow.fiber);
+    return flow.result ?? (yield* Fiber.await(flow.fiber));
+  });
+
   const stopFlow = (flow: AuthFlow, phase: "cancelled" | "failed", message: string) =>
     Effect.uninterruptible(
       Effect.gen(function* () {
-        const detached = yield* lock.withPermits(1)(
+        const stopping = yield* lock.withPermits(1)(
           Effect.gen(function* () {
-            if (activeFlow !== flow) return false;
-            activeFlow = undefined;
+            if (activeFlow !== flow || operation !== "auth") return false;
             operation = "cancel";
             flow.pending = undefined;
             yield* publishFlow(flow, {
               ...flow.state,
-              phase,
+              phase: "verifying",
               authorizationUrl: null,
               expiresAt: null,
-              message,
+              message: "Stopping Google sign-in.",
             });
             return true;
           }),
         );
+        if (!stopping) return;
+        const result = yield* interruptFlow(flow);
-        if (!detached) return;
-        if (flow.forwarding) yield* Fiber.interrupt(flow.forwarding);
-        if (flow.fiber) yield* Fiber.interrupt(flow.fiber);
         yield* lock.withPermits(1)(
+          Effect.gen(function* () {
+            if (activeFlow !== flow || operation !== "cancel") return;
+            activeFlow = undefined;
-          Effect.sync(() => {
-            if (operation === "cancel") operation = "idle";
+            operation = "idle";
+            const failed = Exit.isFailure(result) && !Cause.hasInterruptsOnly(result.cause);
+            yield* publishFlow(flow, {
+              ...flow.state,
+              phase: failed ? "failed" : phase,
+              message: failed ? safeAuthFailure(result.cause, usesBrowser) : message,
+            });
           }),
         );
       }),
@@ -325,7 +362,12 @@
   const requireFlow = (ownerSessionId: string, flowId: string, name: string) =>
     Effect.gen(function* () {
       const flow = activeFlow;
+      if (
+        !flow ||
+        operation !== "auth" ||
+        flow.id !== flowId ||
-      if (!flow || flow.id !== flowId || flow.ownerSessionId !== ownerSessionId) {
+        flow.ownerSessionId !== ownerSessionId
+      ) {
         return yield* setupError(name, "This sign-in is no longer active in this client.");
       }
       const now = yield* Clock.currentTimeMillis;
@@ -369,6 +411,7 @@
               pending: undefined,
               callbackSent: false,
               fiber: undefined,
+              result: undefined,
               forwarding: undefined,
             };
             activeFlow = flow;
@@ -444,28 +487,38 @@
               }
               operation = "logout";
               const currentFlow = activeFlow;
-              activeFlow = undefined;
               if (currentFlow) {
                 currentFlow.pending = undefined;
                 yield* publishFlow(currentFlow, {
                   ...currentFlow.state,
-                  phase: "cancelled",
+                  phase: "verifying",
                   authorizationUrl: null,
                   expiresAt: null,
-                  message: "Google sign-in was cancelled by sign-out.",
+                  message: "Signing out of Google.",
                 });
               }
               return currentFlow;
             }),
           );
+          let signInResult: Exit.Exit<void, unknown> | undefined;
           const stopRemaining = Effect.gen(function* () {
-            if (flow?.forwarding) yield* Fiber.interrupt(flow.forwarding);
-            if (flow?.fiber) yield* Fiber.interrupt(flow.fiber);
+            if (flow) signInResult = yield* interruptFlow(flow);
+          }).pipe(Effect.ensuring(stopOwnedProcesses));
-            yield* stopOwnedProcesses;
-          });
           const result = yield* restore(
             Effect.gen(function* () {
               yield* stopSessions.pipe(Effect.ensuring(stopRemaining));
+              if (
+                signInResult &&
+                Exit.isFailure(signInResult) &&
+                !Cause.hasInterruptsOnly(signInResult.cause)
+              ) {
+                return yield* new ProviderSetupError({
+                  instanceId: options.instanceId,
+                  operation: "logout",
+                  detail: safeAuthFailure(signInResult.cause, usesBrowser),
+                  cause: signInResult.cause,
+                });
+              }
               const runtime = yield* options.makeRuntime({});
               const initialized = yield* runtime.initialize();
               if (!initialized.agentCapabilities?.auth?.logout) {
@@ -486,6 +539,7 @@
           ).pipe(Effect.exit);
           yield* lock.withPermits(1)(
             Effect.gen(function* () {
+              activeFlow = undefined;
               operation = "idle";
               yield* SubscriptionRef.set(snapshot, {
                 ownerSessionId: null,
```

## apps/server/src/provider/AntigravityCredentials.ts

Status: added; +96; −0

```diff
@@ -1,0 +1,96 @@
+import * as Context from "effect/Context";
+import * as Effect from "effect/Effect";
+import * as FileSystem from "effect/FileSystem";
+import * as Layer from "effect/Layer";
+import * as Path from "effect/Path";
+import * as Schema from "effect/Schema";
+
+export class AntigravityCredentialsError extends Schema.TaggedError<AntigravityCredentialsError>()(
+  "AntigravityCredentialsError",
+  {
+    operation: Schema.Literals(["recover", "isolate", "discardBackup"]),
+    path: Schema.String,
+    cause: Schema.Defect(),
+  },
+) {
+  override get message(): string {
+    return "Could not update saved Antigravity credentials.";
+  }
+}
+
+export class AntigravityCredentials extends Context.Service<
+  AntigravityCredentials,
+  {
+    readonly recover: Effect.Effect<void, AntigravityCredentialsError>;
+    readonly discardBackup: Effect.Effect<void, AntigravityCredentialsError>;
+    readonly withFreshSignIn: <A, E, R>(
+      task: Effect.Effect<A, E, R>,
+    ) => Effect.Effect<A, E | AntigravityCredentialsError, R>;
+  }
+>()("t3/provider/AntigravityCredentials") {}
+
+const make = Effect.fn("AntigravityCredentials.make")(function* (profileDirectory: string) {
+  const fs = yield* FileSystem.FileSystem;
+  const path = yield* Path.Path;
+  const tokenPath = path.join(profileDirectory, "antigravity-acp", "acp_token.json");
+  const backupPath = `${tokenPath}.before-sign-in`;
+  const discardBackup = fs.remove(backupPath, { force: true }).pipe(
+    Effect.mapError(
+      (cause) =>
+        new AntigravityCredentialsError({
+          operation: "discardBackup",
+          path: backupPath,
+          cause,
+        }),
+    ),
+  );
+  const recover = Effect.gen(function* () {
+    if (!(yield* fs.exists(backupPath))) return;
+    if (!(yield* fs.exists(tokenPath))) {
+      // Both names share a directory. A hard link installs atomically without replacing a native writer.
+      yield* fs.link(backupPath, tokenPath).pipe(
+        Effect.catchTags({
+          PlatformError: (cause) =>
+            cause.reason._tag === "AlreadyExists" ? Effect.void : Effect.fail(cause),
+        }),
+      );
+    }
+    yield* fs.remove(backupPath, { force: true });
+  }).pipe(
+    Effect.mapError(
+      (cause) =>
+        new AntigravityCredentialsError({
+          operation: "recover",
+          path: tokenPath,
+          cause,
+        }),
+    ),
+  );
+  const withFreshSignIn: AntigravityCredentials["Service"]["withFreshSignIn"] = (task) =>
+    Effect.uninterruptibleMask((restore) =>
+      Effect.gen(function* () {
+        // Settle a previous failed cleanup before its backup name can be reused.
+        yield* recover;
+        yield* Effect.gen(function* () {
+          if (yield* fs.exists(tokenPath)) yield* fs.rename(tokenPath, backupPath);
+        }).pipe(
+          Effect.mapError(
+            (cause) =>
+              new AntigravityCredentialsError({
+                operation: "isolate",
+                path: tokenPath,
+                cause,
+              }),
+          ),
+        );
+        const result = yield* restore(task).pipe(Effect.exit);
+        // Fresh credentials win even when authentication or model discovery subsequently fails.
+        yield* recover;
+        return yield* result;
+      }),
+    );
+  return AntigravityCredentials.of({ recover, discardBackup, withFreshSignIn });
+});
+
+export const layer = (profileDirectory: string) =>
+  Layer.effect(AntigravityCredentials, make(profileDirectory));
```

## apps/server/src/provider/Drivers/AntigravityDriver.ts

Status: modified; +44; −2

```diff
@@ -27,6 +27,7 @@
 } from "../../textGeneration/AntigravityTextGeneration.ts";
 import { makeAntigravityAuth, type AntigravityAuth } from "../AntigravityAuth.ts";
 import * as AntigravityInstallation from "../AntigravityInstallation.ts";
+import * as AntigravityCredentials from "../AntigravityCredentials.ts";
 import * as AntigravityUsage from "./AntigravityUsage.ts";
 import {
   antigravityAuthConfigIssue,
@@ -116,6 +117,7 @@
         gcpLocation: settings.gcpLocation,
       };
       const authConfigIssue = antigravityAuthConfigIssue(auth);
+      const usesBrowser = antigravityAuthUsesBrowser(auth.authMethod);
       const processEnvironment = yield* mergeProviderInstanceEnvironment(environment);
       const userHome = resolveAntigravityUserHome(
         yield* HostProcess.Platform,
@@ -147,6 +149,21 @@
           Effect.provideService(FileSystem.FileSystem, fileSystem),
         );
       }
+      const credentials = yield* AntigravityCredentials.AntigravityCredentials.pipe(
+        Effect.provide(AntigravityCredentials.layer(profileDirectory)),
+      );
+      if (usesBrowser)
+        yield* credentials.recover.pipe(
+          Effect.mapError(
+            (cause) =>
+              new ProviderDriverError({
+                driver: DRIVER,
+                instanceId,
+                detail: "Could not recover saved Antigravity credentials.",
+                cause,
+              }),
+          ),
+        );
       const continuationIdentity = defaultProviderContinuationIdentity({
         driverKind: DRIVER,
         instanceId,
@@ -334,8 +351,33 @@
         instanceId,
         makeRuntime: makeDisposableRuntime,
         onAuthenticated: publishCatalog,
+        onSignedOut: (usesBrowser ? credentials.discardBackup : Effect.void).pipe(
+          Effect.mapError(
+            (cause) =>
+              new ProviderSetupError({
-        onSignedOut: Effect.suspend(() => provider.onSignedOut),
-        usesBrowser: antigravityAuthUsesBrowser(auth.authMethod),
+                instanceId,
+                operation: "logout",
+                detail: "Could not remove the previous Google sign-in. Try signing out again.",
+                cause,
+              }),
+          ),
+          Effect.andThen(Effect.suspend(() => provider.onSignedOut)),
+        ),
+        usesBrowser,
+        withFreshCredentials: (task) =>
+          credentials.withFreshSignIn(task).pipe(
+            Effect.catchTags({
+              AntigravityCredentialsError: (cause) =>
+                Effect.fail(
+                  new ProviderSetupError({
+                    instanceId,
+                    operation: "start",
+                    detail: "Could not update the saved Google sign-in. Try again.",
+                    cause,
+                  }),
+                ),
+            }),
+          ),
       });
 
       // The probe must not spawn. The agent is a PyInstaller one-file bundle
```

## apps/server/src/provider/antigravityAuthSupport.test.ts

Status: modified; +103; −0

```diff
@@ -37,6 +37,8 @@
   prepareAntigravityProfile,
   resolveAntigravityInstanceDirectories,
 } from "./antigravityAuthSupport.ts";
+
+import * as AntigravityCredentials from "./AntigravityCredentials.ts";
 
 const authorizationUrl =
   "https://accounts.google.com/o/oauth2/v2/auth?response_type=code" +
@@ -756,3 +758,104 @@
     }),
   );
 });
+
+it.layer(NodeServices.layer)("Antigravity credential recovery", (it) => {
+  const makeProfile = Effect.gen(function* () {
+    const fs = yield* FileSystem.FileSystem;
+    const path = yield* Path.Path;
+    const profileDirectory = yield* fs.makeTempDirectoryScoped();
+    const directory = path.join(profileDirectory, "antigravity-acp");
+    yield* fs.makeDirectory(directory);
+    const tokenPath = path.join(directory, "acp_token.json");
+    const backupPath = `${tokenPath}.before-sign-in`;
+    yield* fs.writeFileString(backupPath, "previous-account");
+    return { fs, profileDirectory, tokenPath, backupPath };
+  });
+
+  it.effect("recovers an interrupted sign-in and retires backups when a newer token exists", () =>
+    Effect.gen(function* () {
+      const profile = yield* makeProfile;
+      const credentials = yield* AntigravityCredentials.AntigravityCredentials.pipe(
+        Effect.provide(AntigravityCredentials.layer(profile.profileDirectory)),
+      );
+      yield* credentials.recover;
+      expect(yield* profile.fs.readFileString(profile.tokenPath)).toBe("previous-account");
+      expect(yield* profile.fs.exists(profile.backupPath)).toBe(false);
+
+      yield* profile.fs.writeFileString(profile.backupPath, "previous-account");
+      yield* profile.fs.writeFileString(profile.tokenPath, "chosen-account");
+      yield* credentials.recover;
+      expect(yield* profile.fs.readFileString(profile.tokenPath)).toBe("chosen-account");
+      expect(yield* profile.fs.exists(profile.backupPath)).toBe(false);
+    }),
+  );
+
+  it.effect("keeps both accounts intact when a previous backup cannot be retired", () =>
+    Effect.gen(function* () {
+      const profile = yield* makeProfile;
+      const { fs } = profile;
+      yield* fs.writeFileString(profile.tokenPath, "chosen-account");
+      let failCleanup = true;
+      const credentials = yield* AntigravityCredentials.AntigravityCredentials.pipe(
+        Effect.provide(AntigravityCredentials.layer(profile.profileDirectory)),
+        Effect.provideService(
+          FileSystem.FileSystem,
+          FileSystem.FileSystem.of({
+            ...fs,
+            remove: (file, options) =>
+              file === profile.backupPath && failCleanup
+                ? fs.remove(profile.profileDirectory)
+                : fs.remove(file, options),
+          }),
+        ),
+      );
+      let started = false;
+      const result = yield* credentials
+        .withFreshSignIn(
+          Effect.sync(() => {
+            started = true;
+          }),
+        )
+        .pipe(Effect.result);
+      expect(Result.isFailure(result)).toBe(true);
+      expect(started).toBe(false);
+      expect(yield* fs.readFileString(profile.tokenPath)).toBe("chosen-account");
+      expect(yield* fs.readFileString(profile.backupPath)).toBe("previous-account");
+
+      failCleanup = false;
+      yield* credentials.withFreshSignIn(
+        Effect.gen(function* () {
+          expect(yield* fs.exists(profile.tokenPath)).toBe(false);
+          expect(yield* fs.readFileString(profile.backupPath)).toBe("chosen-account");
+        }),
+      );
+      expect(yield* fs.readFileString(profile.tokenPath)).toBe("chosen-account");
+      expect(yield* fs.exists(profile.backupPath)).toBe(false);
+    }),
+  );
+
+  it.effect("preserves credentials written concurrently with recovery", () =>
+    Effect.gen(function* () {
+      const profile = yield* makeProfile;
+      const { fs } = profile;
+      // An orphaned native process can save credentials just before recovery installs its token.
+      const beforeInstall = (destination: string) =>
+        destination === profile.tokenPath
+          ? fs.writeFileString(destination, "chosen-account")
+          : Effect.void;
+      const concurrentFs = FileSystem.FileSystem.of({
+        ...fs,
+        rename: (from, to) => beforeInstall(to).pipe(Effect.andThen(fs.rename(from, to))),
+        link: (from, to) => beforeInstall(to).pipe(Effect.andThen(fs.link(from, to))),
+        copyFile: (from, to) => beforeInstall(to).pipe(Effect.andThen(fs.copyFile(from, to))),
+      });
+      const credentials = yield* AntigravityCredentials.AntigravityCredentials.pipe(
+        Effect.provide(AntigravityCredentials.layer(profile.profileDirectory)),
+        Effect.provideService(FileSystem.FileSystem, concurrentFs),
+      );
+      yield* credentials.recover;
+      expect(yield* fs.readFileString(profile.tokenPath)).toBe("chosen-account");
+      expect(yield* fs.exists(profile.backupPath)).toBe(false);
+    }),
+  );
+});
```

## docs/user/providers-antigravity.md

Status: modified; +6; −3

```diff
@@ -46,8 +46,10 @@
 The environment stores the Antigravity API key outside its settings file, and
 clients only see whether one is saved. The agent uses the method and credentials selected for this instance;
 ambient `GEMINI_API_KEY` and Google credential variables do not override them.
+Changing the method stops the instance's sessions. To switch Google accounts,
+choose **Change account**. If you cancel before Google saves a new sign-in,
+T3 Code keeps the current account. Once a new sign-in is saved, T3 Code keeps it
-Changing the method stops the instance's sessions. Sign out before replacing an
+even if you cancel or model discovery fails.
-account.
 
 ## Runtime installation
 
@@ -117,7 +119,8 @@
 
 All three keep thread history and workspace files. Sending `/logout` by itself in
 a thread signs out its instance, including stopping that instance's other sessions.
-Sign out, then sign in again to replace an account.
+To replace an account, choose **Change account**. Cancelling before Google saves
+a new sign-in keeps the current account.
 
 Before removing a managed runtime, disable its instances and cancel any active
 installation. Clear any explicit binary path pointing into that runtime. Removal
```
