From 65450df5a68d5e8488ebba39439a52bad2304b24 Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Sun, 25 Oct 2026 08:12:41 -0500 Subject: [PATCH 1/6] fix(server): Antigravity Agent Platform says when Google Cloud credentials are missing Agent Platform without an API key relies on Google Cloud application default credentials, but the Antigravity runtime starts sessions without them, so T3 reported the provider as authenticated and the first prompt failed with a raw credential error. The provider check and every runtime start now look for the credentials the runtime would find and report how to create them instead. Co-Authored-By: Claude Opus 5.5 --- apps/server/scripts/acp-mock-agent.ts | 3 +- .../Drivers/AntigravityDriver.test.ts | 38 +++++++++++++++- .../src/provider/Drivers/AntigravityDriver.ts | 17 +++++-- .../src/provider/antigravityAuthSupport.ts | 44 ++++++++++++++++++- 4 files changed, 96 insertions(+), 6 deletions(-) diff --git a/apps/server/scripts/acp-mock-agent.ts b/apps/server/scripts/acp-mock-agent.ts index 51e89523d031e..31ce0be440d9e 100644 --- a/apps/server/scripts/acp-mock-agent.ts +++ b/apps/server/scripts/acp-mock-agent.ts @@ -599,12 +599,13 @@ const program = Effect.gen(function* () { }), ); - // Antigravity requires GEMINI_API_KEY for API key authentication. + // Antigravity requires GEMINI_API_KEY for API key authentication; Agent Platform checks credentials only on prompt. yield* agent.handleAuthenticate((request) => Effect.gen(function* () { if (antigravityProfile) { if ( request.methodId !== "oauth-personal" && + request.methodId !== "agent-platform" && !(request.methodId === "gemini-api-key" && process.env.GEMINI_API_KEY) ) { return yield* AcpError.AcpRequestError.invalidParams( diff --git a/apps/server/src/provider/Drivers/AntigravityDriver.test.ts b/apps/server/src/provider/Drivers/AntigravityDriver.test.ts index 74f4daa1323af..786670aa878ad 100644 --- a/apps/server/src/provider/Drivers/AntigravityDriver.test.ts +++ b/apps/server/src/provider/Drivers/AntigravityDriver.test.ts @@ -10,6 +10,7 @@ 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 Result from "effect/Result"; import * as Schema from "effect/Schema"; import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; import { HttpClient } from "effect/http"; @@ -52,7 +53,11 @@ function shellQuote(value: string): string { } const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* ( - options: { readonly config?: Partial; readonly enabled?: boolean } = {}, + options: { + readonly config?: Partial; + readonly enabled?: boolean; + readonly environment?: ReadonlyArray<{ readonly name: string; readonly value: string }>; + } = {}, ) { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; @@ -195,6 +200,7 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* ( { name: "GEMINI_HOME", value: "/must-not-be-used" }, { name: "ANTIGRAVITY_HARNESS_PATH", value: "/must-not-be-used" }, { name: "BROWSER", value: "must-not-run" }, + ...(options.environment ?? []), ].map((variable) => ({ ...variable, sensitive: false })), }).pipe( Effect.provide(layerInstallation), @@ -353,6 +359,36 @@ it.layer(layerTest)("AntigravityDriver", (it) => { }).pipe(Effect.scoped), ); + it.effect.skipIf(windowsHost)( + "reports missing Google Cloud credentials before Agent Platform starts", + () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const home = yield* fs.makeTempDirectoryScoped({ prefix: "t3-antigravity-home-" }); + const h = yield* makeHarness({ + config: { authMethod: "agent-platform", gcpProject: "proj", gcpLocation: "us-central1" }, + environment: [{ name: "HOME", value: home }], + }); + const result = yield* h.refresh().pipe(Effect.result); + expect(Result.isFailure(result) ? result.failure.detail : "refresh succeeded").toContain( + "gcloud auth application-default login", + ); + expect((yield* h.instance.snapshot.getSnapshot).auth.status).not.toBe("authenticated"); + expect(h.launches).toEqual([]); + + const gcloudDirectory = path.join(home, ".config", "gcloud"); + yield* fs.makeDirectory(gcloudDirectory, { recursive: true }); + yield* fs.writeFileString( + path.join(gcloudDirectory, "application_default_credentials.json"), + "{}", + ); + yield* h.refresh(); + expect((yield* h.instance.snapshot.getSnapshot).auth.status).toBe("authenticated"); + yield* h.assertClosed; + }).pipe(Effect.scoped), + ); + it.effect.skipIf(windowsHost)("keeps the previous catalog when executable resolution fails", () => Effect.gen(function* () { const h = yield* makeHarness(); diff --git a/apps/server/src/provider/Drivers/AntigravityDriver.ts b/apps/server/src/provider/Drivers/AntigravityDriver.ts index fe974dee763ff..742a89ef63d99 100644 --- a/apps/server/src/provider/Drivers/AntigravityDriver.ts +++ b/apps/server/src/provider/Drivers/AntigravityDriver.ts @@ -29,7 +29,7 @@ import { makeAntigravityAuth, type AntigravityAuth } from "../AntigravityAuth.ts import * as AntigravityInstallation from "../AntigravityInstallation.ts"; import * as AntigravityUsage from "./AntigravityUsage.ts"; import { - antigravityAuthConfigIssue, + antigravityAuthIssue, antigravityAuthLabel, antigravityAuthUsesBrowser, buildAntigravityAcpSpawnInput, @@ -121,13 +121,22 @@ export const AntigravityDriver: ProviderDriver< gcpProject: settings.gcpProject, gcpLocation: settings.gcpLocation, }; - const authConfigIssue = antigravityAuthConfigIssue(auth); const processEnvironment = yield* mergeProviderInstanceEnvironment(environment); + const platform = yield* HostProcess.Platform; const userHome = resolveAntigravityUserHome( - yield* HostProcess.Platform, + platform, processEnvironment, yield* HostProcess.HomeDirectory, ); + // Recheck on every use so credentials created after startup take effect. + const authIssue = antigravityAuthIssue(auth, { + platform, + environment: processEnvironment, + userHome, + }).pipe( + Effect.provideService(FileSystem.FileSystem, fileSystem), + Effect.provideService(Path.Path, path), + ); const directories = yield* resolveAntigravityInstanceDirectories( host.paths.stateDir, instanceId, @@ -176,6 +185,7 @@ export const AntigravityDriver: ProviderDriver< AcpError | ProviderSetupError, Scope.Scope > { + const authConfigIssue = yield* authIssue; if (authConfigIssue !== null) { return yield* new ProviderSetupError({ instanceId, @@ -344,6 +354,7 @@ export const AntigravityDriver: ProviderDriver< // minute. const probe = Effect.gen(function* () { yield* modelCatalog.refreshInBackground; + const authConfigIssue = yield* authIssue; if (authConfigIssue !== null) { return yield* new ProviderSetupError({ instanceId, diff --git a/apps/server/src/provider/antigravityAuthSupport.ts b/apps/server/src/provider/antigravityAuthSupport.ts index 5030b53005e89..0bae50287aa95 100644 --- a/apps/server/src/provider/antigravityAuthSupport.ts +++ b/apps/server/src/provider/antigravityAuthSupport.ts @@ -50,6 +50,8 @@ const ProfileSettingsFile = Schema.Struct({ const encodeProfileSettings = Schema.encodeSync(Schema.fromJsonString(ProfileSettingsFile)); const isAcpRequestError = Schema.is(AcpErrors.AcpRequestError); const isAcpTransportError = Schema.is(AcpErrors.AcpTransportError); +// Google Compute Engine VMs get credentials from the metadata server. +const googleCloudProductNames = new Set(["Google", "Google Compute Engine"]); // Python splits BROWSER on path separators before parsing quotes. Exit 0 on EPIPE to prevent browser fallback after cancellation. const browserHelperSource = @@ -119,7 +121,7 @@ export function antigravityAuthLabel(authMethod: AntigravityAuthMethod): string } } -export function antigravityAuthConfigIssue(auth: AntigravityAuthConfig): string | null { +function antigravityAuthConfigIssue(auth: AntigravityAuthConfig): string | null { switch (auth.authMethod) { case "oauth-personal": return null; @@ -136,6 +138,46 @@ export function antigravityAuthConfigIssue(auth: AntigravityAuthConfig): string } } +/** + * The runtime starts sessions without credentials and fails on the first prompt. Mirror its Go + * credential lookup, minus GOOGLE_APPLICATION_CREDENTIALS, which the launch environment drops. + */ +export const antigravityAuthIssue = Effect.fn("antigravityAuthIssue")(function* ( + auth: AntigravityAuthConfig, + host: { + readonly platform: NodeJS.Platform; + readonly environment: NodeJS.ProcessEnv; + readonly userHome: string; + }, +) { + const configIssue = antigravityAuthConfigIssue(auth); + if (configIssue !== null || auth.authMethod !== "agent-platform" || auth.apiKey) { + return configIssue; + } + if (host.environment.GCE_METADATA_HOST) return null; + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const configDirectory = + host.platform === "win32" ? host.environment.APPDATA : path.join(host.userHome, ".config"); + if ( + configDirectory && + (yield* fs + .exists(path.join(configDirectory, "gcloud", "application_default_credentials.json")) + .pipe(Effect.orElseSucceed(() => false))) + ) { + return null; + } + const productName = + host.platform === "linux" + ? yield* fs + .readFileString("/sys/class/dmi/id/product_name") + .pipe(Effect.orElseSucceed(() => "")) + : ""; + return googleCloudProductNames.has(productName.trim()) + ? null + : "Agent Platform without an API key needs Google Cloud application default credentials. Run `gcloud auth application-default login`, or enter an API key in the Antigravity provider settings."; +}); + /** * `auth.type` names the selected method so a native logout clears only that method's credentials * instead of every stored token. Never holds a credential. From 29791cb5aacdb5cb910c725db969df6cd01f12cd Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Sun, 25 Oct 2026 11:14:33 -0500 Subject: [PATCH 2/6] fix(server): Agent Platform credential check honors CLOUDSDK_CONFIG The runtime's Google credential lookup reads application default credentials from CLOUDSDK_CONFIG when it is set, as gcloud writes them there. Without it, the new check blocked Agent Platform for users whose gcloud config lives outside ~/.config/gcloud. Co-Authored-By: Claude Opus 5.5 --- apps/server/scripts/acp-mock-agent.ts | 2 +- .../provider/antigravityAuthSupport.test.ts | 39 +++++++++++++++++++ .../src/provider/antigravityAuthSupport.ts | 11 ++++-- 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/apps/server/scripts/acp-mock-agent.ts b/apps/server/scripts/acp-mock-agent.ts index 31ce0be440d9e..786d6f660d0bf 100644 --- a/apps/server/scripts/acp-mock-agent.ts +++ b/apps/server/scripts/acp-mock-agent.ts @@ -599,7 +599,7 @@ const program = Effect.gen(function* () { }), ); - // Antigravity requires GEMINI_API_KEY for API key authentication; Agent Platform checks credentials only on prompt. + // Antigravity requires GEMINI_API_KEY for API key authentication. Agent Platform checks credentials on prompt. yield* agent.handleAuthenticate((request) => Effect.gen(function* () { if (antigravityProfile) { diff --git a/apps/server/src/provider/antigravityAuthSupport.test.ts b/apps/server/src/provider/antigravityAuthSupport.test.ts index 9138a28dab5b3..adb823a94a348 100644 --- a/apps/server/src/provider/antigravityAuthSupport.test.ts +++ b/apps/server/src/provider/antigravityAuthSupport.test.ts @@ -21,6 +21,7 @@ import { ANTIGRAVITY_SIGN_IN_REQUIRED_MESSAGE, type AntigravityAuthConfig, type AntigravityProfile, + antigravityAuthIssue, buildAntigravityAcpSpawnInput, isAntigravitySignInRequiredError, makeAntigravityStderrHandler, @@ -657,3 +658,41 @@ it.layer(NodeServices.layer)("Antigravity profile preparation", (it) => { }), ); }); + +it.layer(NodeServices.layer)("Antigravity Agent Platform credentials", (it) => { + it.effect("finds application default credentials where the runtime looks for them", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const userHome = yield* fs.makeTempDirectoryScoped(); + const sdkConfig = path.join(userHome, "sdk-config"); + yield* fs.makeDirectory(sdkConfig); + yield* fs.makeDirectory(path.join(userHome, ".config", "gcloud"), { recursive: true }); + yield* fs.writeFileString( + path.join(userHome, ".config", "gcloud", "application_default_credentials.json"), + "{}", + ); + const auth: AntigravityAuthConfig = { + authMethod: "agent-platform", + apiKey: "", + gcpProject: "proj", + gcpLocation: "us-central1", + }; + const issueWith = (environment: NodeJS.ProcessEnv) => + antigravityAuthIssue(auth, { platform: "darwin", environment, userHome }); + + // CLOUDSDK_CONFIG replaces ~/.config/gcloud, as it does for gcloud. + expect(yield* issueWith({ CLOUDSDK_CONFIG: sdkConfig })).toContain( + "gcloud auth application-default login", + ); + yield* fs.writeFileString(path.join(sdkConfig, "application_default_credentials.json"), "{}"); + expect(yield* issueWith({ CLOUDSDK_CONFIG: sdkConfig })).toBeNull(); + expect( + yield* issueWith({ + CLOUDSDK_CONFIG: path.join(userHome, "missing"), + GCE_METADATA_HOST: "metadata.google.internal", + }), + ).toBeNull(); + }), + ); +}); diff --git a/apps/server/src/provider/antigravityAuthSupport.ts b/apps/server/src/provider/antigravityAuthSupport.ts index 0bae50287aa95..c74679e5d8ddf 100644 --- a/apps/server/src/provider/antigravityAuthSupport.ts +++ b/apps/server/src/provider/antigravityAuthSupport.ts @@ -157,12 +157,15 @@ export const antigravityAuthIssue = Effect.fn("antigravityAuthIssue")(function* if (host.environment.GCE_METADATA_HOST) return null; const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const configDirectory = - host.platform === "win32" ? host.environment.APPDATA : path.join(host.userHome, ".config"); + const gcloudDirectory = + host.environment.CLOUDSDK_CONFIG || + (host.platform === "win32" + ? host.environment.APPDATA && path.join(host.environment.APPDATA, "gcloud") + : path.join(host.userHome, ".config", "gcloud")); if ( - configDirectory && + gcloudDirectory && (yield* fs - .exists(path.join(configDirectory, "gcloud", "application_default_credentials.json")) + .exists(path.join(gcloudDirectory, "application_default_credentials.json")) .pipe(Effect.orElseSucceed(() => false))) ) { return null; From fdc81d5c81730f3b71bc21e542ddde6399cd4ec7 Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Sun, 25 Oct 2026 12:19:09 -0500 Subject: [PATCH 3/6] fix(server): Agent Platform accepts metadata server credentials and still signs out Probe the Google metadata server, once per instance and bounded to one second, before reporting missing application default credentials, as the runtime's Go client does. This replaces the Linux DMI guess, which rejected valid credentials on Windows VMs and on hosts with an unknown product name. Sign-out no longer checks for Google Cloud credentials, so removing them after sign-in does not block it. Driver tests pin CLOUDSDK_CONFIG and GCE_METADATA_HOST so the caller's shell cannot change their result. Co-Authored-By: Claude Opus 5.5 --- apps/server/scripts/acp-mock-agent.ts | 7 ++ apps/server/src/provider/AntigravityAuth.ts | 3 +- .../Drivers/AntigravityDriver.test.ts | 82 ++++++++++++++++++- .../src/provider/Drivers/AntigravityDriver.ts | 30 +++++-- .../provider/antigravityAuthSupport.test.ts | 64 ++++++++++++++- .../src/provider/antigravityAuthSupport.ts | 21 ++--- 6 files changed, 185 insertions(+), 22 deletions(-) diff --git a/apps/server/scripts/acp-mock-agent.ts b/apps/server/scripts/acp-mock-agent.ts index 786d6f660d0bf..5ff9834a5db7f 100644 --- a/apps/server/scripts/acp-mock-agent.ts +++ b/apps/server/scripts/acp-mock-agent.ts @@ -2585,6 +2585,13 @@ const program = Effect.gen(function* () { ); yield* agent.handleUnknownExtRequest((method, params) => { + // Antigravity's sign-out is the raw v1 method, sent through the runtime's generic request. + if (antigravityProfile && method === "logout") { + return Effect.sync(() => { + authenticated = false; + return {}; + }); + } if (method === "_test/environment") { return Effect.succeed({ inherited: process.env.T3_ACP_RUNTIME_AMBIENT === "sentinel", diff --git a/apps/server/src/provider/AntigravityAuth.ts b/apps/server/src/provider/AntigravityAuth.ts index a2026faa78d26..a8df59191896a 100644 --- a/apps/server/src/provider/AntigravityAuth.ts +++ b/apps/server/src/provider/AntigravityAuth.ts @@ -82,6 +82,7 @@ export interface AntigravityAuthOptions< readonly instanceId: ProviderInstanceId; readonly makeRuntime: (input: { readonly onAuthorizationUrl?: (url: string) => Effect.Effect; + readonly signOut?: boolean; }) => Effect.Effect; readonly onAuthenticated: ( result: AcpSessionRuntime.AcpSessionRuntimeStartResult, @@ -497,7 +498,7 @@ export const makeAntigravityAuth = Effect.fn("makeAntigravityAuth")(function* < const result = yield* restore( Effect.gen(function* () { yield* stopSessions.pipe(Effect.ensuring(stopRemaining)); - const runtime = yield* options.makeRuntime({}); + const runtime = yield* options.makeRuntime({ signOut: true }); const initialized = yield* runtime.initialize(); if (!initialized.agentCapabilities?.auth?.logout) { return yield* setupError( diff --git a/apps/server/src/provider/Drivers/AntigravityDriver.test.ts b/apps/server/src/provider/Drivers/AntigravityDriver.test.ts index 786670aa878ad..4320ce8dbfd97 100644 --- a/apps/server/src/provider/Drivers/AntigravityDriver.test.ts +++ b/apps/server/src/provider/Drivers/AntigravityDriver.test.ts @@ -13,7 +13,7 @@ import * as Path from "effect/Path"; import * as Result from "effect/Result"; import * as Schema from "effect/Schema"; import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; -import { HttpClient } from "effect/http"; +import { HttpClient, HttpClientError, HttpClientResponse } from "effect/http"; import * as BackgroundPolicy from "../../background/BackgroundPolicy.ts"; import * as ServerConfig from "../../config.ts"; @@ -57,6 +57,7 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* ( readonly config?: Partial; readonly enabled?: boolean; readonly environment?: ReadonlyArray<{ readonly name: string; readonly value: string }>; + readonly metadataServer?: boolean; } = {}, ) { const fs = yield* FileSystem.FileSystem; @@ -159,6 +160,25 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* ( return selected; }), }); + const metadataRequests: Array = []; + const httpClient = HttpClient.make((request) => { + if (!request.url.startsWith("http://169.254.169.254")) { + return Effect.die("Unexpected quota request"); + } + metadataRequests.push(request.url); + return options.metadataServer + ? Effect.succeed( + HttpClientResponse.fromWeb( + request, + new Response(null, { headers: { "Metadata-Flavor": "Google" } }), + ), + ) + : Effect.fail( + new HttpClientError.HttpClientError({ + reason: new HttpClientError.TransportError({ request }), + }), + ); + }); const observedSpawner = ChildProcessSpawner.make((command) => Effect.gen(function* () { if (command._tag !== "StandardCommand") @@ -200,11 +220,14 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* ( { name: "GEMINI_HOME", value: "/must-not-be-used" }, { name: "ANTIGRAVITY_HARNESS_PATH", value: "/must-not-be-used" }, { name: "BROWSER", value: "must-not-run" }, + { name: "CLOUDSDK_CONFIG", value: "" }, + { name: "GCE_METADATA_HOST", value: "" }, ...(options.environment ?? []), ].map((variable) => ({ ...variable, sensitive: false })), }).pipe( Effect.provide(layerInstallation), Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, observedSpawner), + Effect.provideService(HttpClient.HttpClient, httpClient), ); const refresh = instance.refreshModels; if (!refresh) return yield* Effect.die("Antigravity does not expose model refresh."); @@ -238,6 +261,7 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* ( acquisitions, releases, launches, + metadataRequests, readRequests, assertClosed, }; @@ -389,6 +413,62 @@ it.layer(layerTest)("AntigravityDriver", (it) => { }).pipe(Effect.scoped), ); + it.effect.skipIf(windowsHost)( + "starts Agent Platform with metadata server credentials and no local credentials file", + () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const home = yield* fs.makeTempDirectoryScoped({ prefix: "t3-antigravity-home-" }); + const h = yield* makeHarness({ + config: { authMethod: "agent-platform", gcpProject: "proj", gcpLocation: "us-central1" }, + environment: [{ name: "HOME", value: home }], + metadataServer: true, + }); + yield* h.refresh(); + yield* h.refresh(); + expect((yield* h.instance.snapshot.getSnapshot).auth.status).toBe("authenticated"); + expect(h.metadataRequests).toHaveLength(1); + yield* h.assertClosed; + }).pipe(Effect.scoped), + ); + + it.effect.skipIf(windowsHost)( + "signs out of Agent Platform after its Google Cloud credentials are removed", + () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const home = yield* fs.makeTempDirectoryScoped({ prefix: "t3-antigravity-home-" }); + const credentials = path.join( + home, + ".config", + "gcloud", + "application_default_credentials.json", + ); + yield* fs.makeDirectory(path.dirname(credentials), { recursive: true }); + yield* fs.writeFileString(credentials, "{}"); + const h = yield* makeHarness({ + config: { authMethod: "agent-platform", gcpProject: "proj", gcpLocation: "us-central1" }, + // The mock advertises sign-out only when it lists an auth method. + environment: [ + { name: "HOME", value: home }, + { name: "T3_ACP_AUTH_METHOD_ID", value: "agent-platform" }, + ], + }); + const auth = h.instance.auth; + if (!auth) return yield* Effect.die("Antigravity does not expose sign-in."); + yield* h.refresh(); + expect((yield* h.instance.snapshot.getSnapshot).auth.status).toBe("authenticated"); + + yield* fs.remove(credentials); + const state = yield* auth.logout(Effect.void); + expect(state.message).toBe("Signed out of Google."); + expect((yield* h.instance.snapshot.getSnapshot).auth.status).toBe("unauthenticated"); + expect((yield* h.readRequests).map((request) => request.method)).toContain("logout"); + yield* h.assertClosed; + }).pipe(Effect.scoped), + ); + it.effect.skipIf(windowsHost)("keeps the previous catalog when executable resolution fails", () => Effect.gen(function* () { const h = yield* makeHarness(); diff --git a/apps/server/src/provider/Drivers/AntigravityDriver.ts b/apps/server/src/provider/Drivers/AntigravityDriver.ts index 742a89ef63d99..6de50bc81dd54 100644 --- a/apps/server/src/provider/Drivers/AntigravityDriver.ts +++ b/apps/server/src/provider/Drivers/AntigravityDriver.ts @@ -29,12 +29,14 @@ import { makeAntigravityAuth, type AntigravityAuth } from "../AntigravityAuth.ts import * as AntigravityInstallation from "../AntigravityInstallation.ts"; import * as AntigravityUsage from "./AntigravityUsage.ts"; import { + antigravityAuthConfigIssue, antigravityAuthIssue, antigravityAuthLabel, antigravityAuthUsesBrowser, buildAntigravityAcpSpawnInput, isAntigravitySignInRequiredError, prepareAntigravityProfile, + probeGoogleMetadataServer, resolveAntigravityInstanceDirectories, type AntigravityAuthConfig, } from "../antigravityAuthSupport.ts"; @@ -128,11 +130,16 @@ export const AntigravityDriver: ProviderDriver< processEnvironment, yield* HostProcess.HomeDirectory, ); + // Probe the metadata server at most once per instance, as the Go client does per process. + const onGoogleCloud = yield* Effect.cached( + probeGoogleMetadataServer.pipe(Effect.provideService(HttpClient.HttpClient, httpClient)), + ); // Recheck on every use so credentials created after startup take effect. const authIssue = antigravityAuthIssue(auth, { platform, environment: processEnvironment, userHome, + onGoogleCloud, }).pipe( Effect.provideService(FileSystem.FileSystem, fileSystem), Effect.provideService(Path.Path, path), @@ -180,12 +187,14 @@ export const AntigravityDriver: ProviderDriver< const makeRuntime = Effect.fn("AntigravityDriver.makeRuntime")(function* ( input: Omit, + options: { readonly signOut?: boolean } = {}, ): Effect.fn.Return< AcpSessionRuntime.AcpSessionRuntime["Service"], AcpError | ProviderSetupError, Scope.Scope > { - const authConfigIssue = yield* authIssue; + // Sign-out needs no Google Cloud credentials, so removing them must not block it. + const authConfigIssue = options.signOut ? antigravityAuthConfigIssue(auth) : yield* authIssue; if (authConfigIssue !== null) { return yield* new ProviderSetupError({ instanceId, @@ -278,7 +287,9 @@ export const AntigravityDriver: ProviderDriver< }); const makeDisposableRuntime = Effect.fn("AntigravityDriver.makeDisposableRuntime")(function* ( - input: Pick, + input: Pick & { + readonly signOut?: boolean; + }, ) { const cwd = yield* fileSystem .makeTempDirectoryScoped({ prefix: "t3-antigravity-setup-" }) @@ -303,12 +314,15 @@ export const AntigravityDriver: ProviderDriver< Effect.provideService(Path.Path, path), ), ); - const runtime = yield* makeRuntime({ - cwd, - clientInfo: { name: "t3-code-provider-setup", version: "0.0.0" }, - mcpServers: [], - ...(input.onAuthorizationUrl ? { onAuthorizationUrl: input.onAuthorizationUrl } : {}), - }); + const runtime = yield* makeRuntime( + { + cwd, + clientInfo: { name: "t3-code-provider-setup", version: "0.0.0" }, + mcpServers: [], + ...(input.onAuthorizationUrl ? { onAuthorizationUrl: input.onAuthorizationUrl } : {}), + }, + { signOut: input.signOut === true }, + ); return { ...runtime, start: () => diff --git a/apps/server/src/provider/antigravityAuthSupport.test.ts b/apps/server/src/provider/antigravityAuthSupport.test.ts index adb823a94a348..d8472b0f7774a 100644 --- a/apps/server/src/provider/antigravityAuthSupport.test.ts +++ b/apps/server/src/provider/antigravityAuthSupport.test.ts @@ -5,12 +5,15 @@ import { ProviderInstanceId } from "@t3tools/contracts"; import * as HostProcess from "@t3tools/shared/HostProcess"; import { describe, expect, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; +import * as Fiber from "effect/Fiber"; import * as FileSystem from "effect/FileSystem"; +import { HttpClient, HttpClientError, HttpClientResponse } from "effect/http"; import * as Layer from "effect/Layer"; import * as Path from "effect/Path"; import * as Result from "effect/Result"; import * as Schema from "effect/Schema"; import * as Stream from "effect/Stream"; +import * as TestClock from "effect/testing/TestClock"; import * as Ndjson from "effect/encoding/Ndjson"; import * as AcpErrors from "effect-acp/errors"; import { symlinksSupported } from "@t3tools/shared/testing/symlinks"; @@ -28,6 +31,7 @@ import { makeAntigravityStdoutTransform, parseAntigravityAuthorizationUrl, prepareAntigravityProfile, + probeGoogleMetadataServer, resolveAntigravityInstanceDirectories, } from "./antigravityAuthSupport.ts"; @@ -678,8 +682,17 @@ it.layer(NodeServices.layer)("Antigravity Agent Platform credentials", (it) => { gcpProject: "proj", gcpLocation: "us-central1", }; - const issueWith = (environment: NodeJS.ProcessEnv) => - antigravityAuthIssue(auth, { platform: "darwin", environment, userHome }); + const issueWith = ( + environment: NodeJS.ProcessEnv, + platform: NodeJS.Platform = "darwin", + onGoogleCloud = false, + ) => + antigravityAuthIssue(auth, { + platform, + environment, + userHome, + onGoogleCloud: Effect.succeed(onGoogleCloud), + }); // CLOUDSDK_CONFIG replaces ~/.config/gcloud, as it does for gcloud. expect(yield* issueWith({ CLOUDSDK_CONFIG: sdkConfig })).toContain( @@ -693,6 +706,53 @@ it.layer(NodeServices.layer)("Antigravity Agent Platform credentials", (it) => { GCE_METADATA_HOST: "metadata.google.internal", }), ).toBeNull(); + + // Windows reads %APPDATA%\gcloud and never the POSIX path. + expect(yield* issueWith({ APPDATA: userHome }, "win32")).toContain( + "gcloud auth application-default login", + ); + yield* fs.makeDirectory(path.join(userHome, "gcloud")); + yield* fs.writeFileString( + path.join(userHome, "gcloud", "application_default_credentials.json"), + "{}", + ); + expect(yield* issueWith({ APPDATA: userHome }, "win32")).toBeNull(); + + // A metadata server supplies credentials without any local file. + const missing = { CLOUDSDK_CONFIG: path.join(userHome, "missing") }; + expect(yield* issueWith(missing, "linux")).toContain("gcloud auth application-default login"); + expect(yield* issueWith(missing, "linux", true)).toBeNull(); + expect( + yield* issueWith({ APPDATA: path.join(userHome, "missing") }, "win32", true), + ).toBeNull(); + }), + ); + + it.effect("detects the metadata server the way the Go client does", () => + Effect.gen(function* () { + const probeWith = (respond: Parameters[0]) => + probeGoogleMetadataServer.pipe( + Effect.provideService(HttpClient.HttpClient, HttpClient.make(respond)), + ); + const respondWith = (headers: Record) => + probeWith((request) => + Effect.succeed(HttpClientResponse.fromWeb(request, new Response(null, { headers }))), + ); + + expect(yield* respondWith({ "Metadata-Flavor": "Google" })).toBe(true); + expect(yield* respondWith({})).toBe(false); + expect( + yield* probeWith((request) => + Effect.fail( + new HttpClientError.HttpClientError({ + reason: new HttpClientError.TransportError({ request }), + }), + ), + ), + ).toBe(false); + const unanswered = yield* probeWith(() => Effect.never).pipe(Effect.forkChild); + yield* TestClock.adjust("1 second"); + expect(yield* Fiber.join(unanswered)).toBe(false); }), ); }); diff --git a/apps/server/src/provider/antigravityAuthSupport.ts b/apps/server/src/provider/antigravityAuthSupport.ts index c74679e5d8ddf..4a8c2453ef899 100644 --- a/apps/server/src/provider/antigravityAuthSupport.ts +++ b/apps/server/src/provider/antigravityAuthSupport.ts @@ -8,6 +8,7 @@ import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; import * as Hex from "effect/encoding/Hex"; import * as FileSystem from "effect/FileSystem"; +import { HttpClient } from "effect/http"; import * as Path from "effect/Path"; import type * as PlatformError from "effect/PlatformError"; import * as Schema from "effect/Schema"; @@ -50,8 +51,6 @@ const ProfileSettingsFile = Schema.Struct({ const encodeProfileSettings = Schema.encodeSync(Schema.fromJsonString(ProfileSettingsFile)); const isAcpRequestError = Schema.is(AcpErrors.AcpRequestError); const isAcpTransportError = Schema.is(AcpErrors.AcpTransportError); -// Google Compute Engine VMs get credentials from the metadata server. -const googleCloudProductNames = new Set(["Google", "Google Compute Engine"]); // Python splits BROWSER on path separators before parsing quotes. Exit 0 on EPIPE to prevent browser fallback after cancellation. const browserHelperSource = @@ -121,7 +120,7 @@ export function antigravityAuthLabel(authMethod: AntigravityAuthMethod): string } } -function antigravityAuthConfigIssue(auth: AntigravityAuthConfig): string | null { +export function antigravityAuthConfigIssue(auth: AntigravityAuthConfig): string | null { switch (auth.authMethod) { case "oauth-personal": return null; @@ -148,6 +147,7 @@ export const antigravityAuthIssue = Effect.fn("antigravityAuthIssue")(function* readonly platform: NodeJS.Platform; readonly environment: NodeJS.ProcessEnv; readonly userHome: string; + readonly onGoogleCloud: Effect.Effect; }, ) { const configIssue = antigravityAuthConfigIssue(auth); @@ -170,17 +170,18 @@ export const antigravityAuthIssue = Effect.fn("antigravityAuthIssue")(function* ) { return null; } - const productName = - host.platform === "linux" - ? yield* fs - .readFileString("/sys/class/dmi/id/product_name") - .pipe(Effect.orElseSucceed(() => "")) - : ""; - return googleCloudProductNames.has(productName.trim()) + return (yield* host.onGoogleCloud) ? null : "Agent Platform without an API key needs Google Cloud application default credentials. Run `gcloud auth application-default login`, or enter an API key in the Antigravity provider settings."; }); +// The Go client's metadata server check. Unreachable link-local addresses can hang, so bound it. +export const probeGoogleMetadataServer = HttpClient.get("http://169.254.169.254").pipe( + Effect.map((response) => response.headers["metadata-flavor"] === "Google"), + Effect.timeoutOrElse({ duration: "1 second", orElse: () => Effect.succeed(false) }), + Effect.orElseSucceed(() => false), +); + /** * `auth.type` names the selected method so a native logout clears only that method's credentials * instead of every stored token. Never holds a credential. From d2e895c1e51e9eb209dcf304729d82e28a565559 Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Sun, 25 Oct 2026 13:26:53 -0500 Subject: [PATCH 4/6] fix(server): Agent Platform metadata check bypasses the outbound proxy With --use-env-proxy or NODE_USE_ENV_PROXY, the metadata server probe went through the global proxy dispatcher. A proxy cannot reach the host's link-local metadata address, so valid metadata credentials were reported as missing. The probe now uses undici's fetch on its own Agent, like the Go client's proxy-free transport; other outbound calls keep the proxy. Co-Authored-By: Claude Opus 5.5 --- apps/server/src/outboundHttp.ts | 9 ++++ .../provider/antigravityAuthSupport.test.ts | 54 ++++++++++++++++++- .../src/provider/antigravityAuthSupport.ts | 26 ++++++--- 3 files changed, 81 insertions(+), 8 deletions(-) diff --git a/apps/server/src/outboundHttp.ts b/apps/server/src/outboundHttp.ts index ead14cb5d2035..749ff2a2e7f2e 100644 --- a/apps/server/src/outboundHttp.ts +++ b/apps/server/src/outboundHttp.ts @@ -26,6 +26,15 @@ export function installHttp1FetchDispatcher(): void { ); } +/** A fetch on its own Agent, so neither the global dispatcher nor an env proxy applies. */ +export function makeDirectFetch() { + const undici = requireUndici("undici") as typeof Undici; + const dispatcher = new undici.Agent(); + const fetch: typeof Undici.fetch = (input, init) => undici.fetch(input, { ...init, dispatcher }); + // The same WHATWG fetch, typed by npm undici rather than the copy behind Node's globals. + return { fetch: fetch as typeof globalThis.fetch, close: () => dispatcher.destroy() }; +} + /** Avoids uncaught idle-timeout errors on h2 streams (https://github.com/nodejs/undici/issues/5936). */ export function keepOutboundFetchOnHttp1(): void { if (builtinFetchUsesHttp2(process.versions.undici)) installHttp1FetchDispatcher(); diff --git a/apps/server/src/provider/antigravityAuthSupport.test.ts b/apps/server/src/provider/antigravityAuthSupport.test.ts index d8472b0f7774a..4d8605d2675bb 100644 --- a/apps/server/src/provider/antigravityAuthSupport.test.ts +++ b/apps/server/src/provider/antigravityAuthSupport.test.ts @@ -1,3 +1,7 @@ +// @effect-diagnostics nodeBuiltinImport:off - real HTTP servers stand in for an outbound proxy and the metadata server. +import * as NodeHttp from "node:http"; +import type * as NodeNet from "node:net"; + import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; import * as NodePath from "@effect/platform-node/NodePath"; import * as NodeServices from "@effect/platform-node/NodeServices"; @@ -7,7 +11,7 @@ import { describe, expect, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as Fiber from "effect/Fiber"; import * as FileSystem from "effect/FileSystem"; -import { HttpClient, HttpClientError, HttpClientResponse } from "effect/http"; +import { FetchHttpClient, HttpClient, HttpClientError, HttpClientResponse } from "effect/http"; import * as Layer from "effect/Layer"; import * as Path from "effect/Path"; import * as Result from "effect/Result"; @@ -16,6 +20,7 @@ import * as Stream from "effect/Stream"; import * as TestClock from "effect/testing/TestClock"; import * as Ndjson from "effect/encoding/Ndjson"; import * as AcpErrors from "effect-acp/errors"; +import * as Undici from "undici"; import { symlinksSupported } from "@t3tools/shared/testing/symlinks"; import { @@ -32,6 +37,7 @@ import { parseAntigravityAuthorizationUrl, prepareAntigravityProfile, probeGoogleMetadataServer, + probeMetadataServer, resolveAntigravityInstanceDirectories, } from "./antigravityAuthSupport.ts"; @@ -756,3 +762,49 @@ it.layer(NodeServices.layer)("Antigravity Agent Platform credentials", (it) => { }), ); }); + +describe("Google metadata server probe", () => { + it.live("reaches the metadata server directly when outbound fetch uses an env proxy", () => + Effect.gen(function* () { + const listen = (handler: NodeHttp.RequestListener) => + Effect.acquireRelease( + Effect.promise(async () => { + const server = NodeHttp.createServer(handler); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + return server; + }), + (server) => Effect.promise(() => new Promise((resolve) => server.close(resolve))), + ).pipe( + Effect.map( + (server) => `http://127.0.0.1:${(server.address() as NodeNet.AddressInfo).port}`, + ), + ); + const metadataServer = yield* listen((_request, response) => + response.writeHead(200, { "Metadata-Flavor": "Google" }).end(), + ); + const proxied: Array = []; + const proxy = yield* listen((request, response) => { + proxied.push(request.url); + response.writeHead(403).end(); + }); + yield* Effect.acquireRelease( + Effect.sync(() => { + const previous = Undici.getGlobalDispatcher(); + const envProxy = new Undici.EnvHttpProxyAgent({ httpProxy: proxy, noProxy: "" }); + Undici.setGlobalDispatcher(envProxy); + return { previous, envProxy }; + }), + ({ previous, envProxy }) => + Effect.promise(() => { + Undici.setGlobalDispatcher(previous); + return envProxy.destroy(); + }), + ); + + const onGoogleCloud = yield* probeMetadataServer(metadataServer).pipe( + Effect.provide(FetchHttpClient.layer), + ); + expect({ onGoogleCloud, proxied }).toEqual({ onGoogleCloud: true, proxied: [] }); + }).pipe(Effect.scoped), + ); +}); diff --git a/apps/server/src/provider/antigravityAuthSupport.ts b/apps/server/src/provider/antigravityAuthSupport.ts index 4a8c2453ef899..73d76679171b7 100644 --- a/apps/server/src/provider/antigravityAuthSupport.ts +++ b/apps/server/src/provider/antigravityAuthSupport.ts @@ -8,7 +8,7 @@ import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; import * as Hex from "effect/encoding/Hex"; import * as FileSystem from "effect/FileSystem"; -import { HttpClient } from "effect/http"; +import { FetchHttpClient, HttpClient } from "effect/http"; import * as Path from "effect/Path"; import type * as PlatformError from "effect/PlatformError"; import * as Schema from "effect/Schema"; @@ -23,6 +23,7 @@ import { antigravityUserSkillDirectories, resolveAntigravityUserHome, } from "./Drivers/AntigravitySkills.ts"; +import { makeDirectFetch } from "../outboundHttp.ts"; export const ANTIGRAVITY_AUTH_STDOUT_PREFIX = "Open the following link to authenticate the ACP server: "; @@ -175,12 +176,23 @@ export const antigravityAuthIssue = Effect.fn("antigravityAuthIssue")(function* : "Agent Platform without an API key needs Google Cloud application default credentials. Run `gcloud auth application-default login`, or enter an API key in the Antigravity provider settings."; }); -// The Go client's metadata server check. Unreachable link-local addresses can hang, so bound it. -export const probeGoogleMetadataServer = HttpClient.get("http://169.254.169.254").pipe( - Effect.map((response) => response.headers["metadata-flavor"] === "Google"), - Effect.timeoutOrElse({ duration: "1 second", orElse: () => Effect.succeed(false) }), - Effect.orElseSucceed(() => false), -); +// The Go client's metadata server check. Like Go's transport it never uses a proxy, which cannot +// reach this host's link-local address. Unreachable link-local addresses can hang, so bound it. +export const probeMetadataServer = (url: string) => + Effect.acquireUseRelease( + Effect.try(makeDirectFetch), + (direct) => + HttpClient.get(url).pipe( + Effect.map((response) => response.headers["metadata-flavor"] === "Google"), + Effect.provideService(FetchHttpClient.Fetch, direct.fetch), + ), + (direct) => Effect.promise(direct.close), + ).pipe( + Effect.timeoutOrElse({ duration: "1 second", orElse: () => Effect.succeed(false) }), + Effect.orElseSucceed(() => false), + ); + +export const probeGoogleMetadataServer = probeMetadataServer("http://169.254.169.254"); /** * `auth.type` names the selected method so a native logout clears only that method's credentials From 729027d5a33dc038eeeb43f523e9410a00116d35 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Mon, 26 Oct 2026 15:51:31 +0000 Subject: [PATCH 5/6] fix(server): verify the configured Agent Platform metadata host --- .../provider/Drivers/AntigravityDriver.test.ts | 11 ++++++++--- .../src/provider/Drivers/AntigravityDriver.ts | 4 +++- .../src/provider/antigravityAuthSupport.test.ts | 15 ++++++++++++--- .../server/src/provider/antigravityAuthSupport.ts | 7 ++----- 4 files changed, 25 insertions(+), 12 deletions(-) diff --git a/apps/server/src/provider/Drivers/AntigravityDriver.test.ts b/apps/server/src/provider/Drivers/AntigravityDriver.test.ts index 4320ce8dbfd97..7fdb77e2ea045 100644 --- a/apps/server/src/provider/Drivers/AntigravityDriver.test.ts +++ b/apps/server/src/provider/Drivers/AntigravityDriver.test.ts @@ -58,6 +58,7 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* ( readonly enabled?: boolean; readonly environment?: ReadonlyArray<{ readonly name: string; readonly value: string }>; readonly metadataServer?: boolean; + readonly metadataHost?: string; } = {}, ) { const fs = yield* FileSystem.FileSystem; @@ -162,7 +163,7 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* ( }); const metadataRequests: Array = []; const httpClient = HttpClient.make((request) => { - if (!request.url.startsWith("http://169.254.169.254")) { + if (!request.url.startsWith(`http://${options.metadataHost ?? "169.254.169.254"}`)) { return Effect.die("Unexpected quota request"); } metadataRequests.push(request.url); @@ -421,13 +422,17 @@ it.layer(layerTest)("AntigravityDriver", (it) => { const home = yield* fs.makeTempDirectoryScoped({ prefix: "t3-antigravity-home-" }); const h = yield* makeHarness({ config: { authMethod: "agent-platform", gcpProject: "proj", gcpLocation: "us-central1" }, - environment: [{ name: "HOME", value: home }], + environment: [ + { name: "HOME", value: home }, + { name: "GCE_METADATA_HOST", value: "metadata.internal:8080" }, + ], + metadataHost: "metadata.internal:8080", metadataServer: true, }); yield* h.refresh(); yield* h.refresh(); expect((yield* h.instance.snapshot.getSnapshot).auth.status).toBe("authenticated"); - expect(h.metadataRequests).toHaveLength(1); + expect(h.metadataRequests).toEqual(["http://metadata.internal:8080"]); yield* h.assertClosed; }).pipe(Effect.scoped), ); diff --git a/apps/server/src/provider/Drivers/AntigravityDriver.ts b/apps/server/src/provider/Drivers/AntigravityDriver.ts index 6de50bc81dd54..a3f741eb651ac 100644 --- a/apps/server/src/provider/Drivers/AntigravityDriver.ts +++ b/apps/server/src/provider/Drivers/AntigravityDriver.ts @@ -132,7 +132,9 @@ export const AntigravityDriver: ProviderDriver< ); // Probe the metadata server at most once per instance, as the Go client does per process. const onGoogleCloud = yield* Effect.cached( - probeGoogleMetadataServer.pipe(Effect.provideService(HttpClient.HttpClient, httpClient)), + probeGoogleMetadataServer(processEnvironment.GCE_METADATA_HOST || undefined).pipe( + Effect.provideService(HttpClient.HttpClient, httpClient), + ), ); // Recheck on every use so credentials created after startup take effect. const authIssue = antigravityAuthIssue(auth, { diff --git a/apps/server/src/provider/antigravityAuthSupport.test.ts b/apps/server/src/provider/antigravityAuthSupport.test.ts index 4d8605d2675bb..876a3db9d6336 100644 --- a/apps/server/src/provider/antigravityAuthSupport.test.ts +++ b/apps/server/src/provider/antigravityAuthSupport.test.ts @@ -37,7 +37,6 @@ import { parseAntigravityAuthorizationUrl, prepareAntigravityProfile, probeGoogleMetadataServer, - probeMetadataServer, resolveAntigravityInstanceDirectories, } from "./antigravityAuthSupport.ts"; @@ -711,6 +710,16 @@ it.layer(NodeServices.layer)("Antigravity Agent Platform credentials", (it) => { CLOUDSDK_CONFIG: path.join(userHome, "missing"), GCE_METADATA_HOST: "metadata.google.internal", }), + ).toContain("gcloud auth application-default login"); + expect( + yield* issueWith( + { + CLOUDSDK_CONFIG: path.join(userHome, "missing"), + GCE_METADATA_HOST: "metadata.google.internal", + }, + "linux", + true, + ), ).toBeNull(); // Windows reads %APPDATA%\gcloud and never the POSIX path. @@ -737,7 +746,7 @@ it.layer(NodeServices.layer)("Antigravity Agent Platform credentials", (it) => { it.effect("detects the metadata server the way the Go client does", () => Effect.gen(function* () { const probeWith = (respond: Parameters[0]) => - probeGoogleMetadataServer.pipe( + probeGoogleMetadataServer().pipe( Effect.provideService(HttpClient.HttpClient, HttpClient.make(respond)), ); const respondWith = (headers: Record) => @@ -801,7 +810,7 @@ describe("Google metadata server probe", () => { }), ); - const onGoogleCloud = yield* probeMetadataServer(metadataServer).pipe( + const onGoogleCloud = yield* probeGoogleMetadataServer(new URL(metadataServer).host).pipe( Effect.provide(FetchHttpClient.layer), ); expect({ onGoogleCloud, proxied }).toEqual({ onGoogleCloud: true, proxied: [] }); diff --git a/apps/server/src/provider/antigravityAuthSupport.ts b/apps/server/src/provider/antigravityAuthSupport.ts index 73d76679171b7..acc4f796c0c1b 100644 --- a/apps/server/src/provider/antigravityAuthSupport.ts +++ b/apps/server/src/provider/antigravityAuthSupport.ts @@ -155,7 +155,6 @@ export const antigravityAuthIssue = Effect.fn("antigravityAuthIssue")(function* if (configIssue !== null || auth.authMethod !== "agent-platform" || auth.apiKey) { return configIssue; } - if (host.environment.GCE_METADATA_HOST) return null; const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const gcloudDirectory = @@ -178,11 +177,11 @@ export const antigravityAuthIssue = Effect.fn("antigravityAuthIssue")(function* // The Go client's metadata server check. Like Go's transport it never uses a proxy, which cannot // reach this host's link-local address. Unreachable link-local addresses can hang, so bound it. -export const probeMetadataServer = (url: string) => +export const probeGoogleMetadataServer = (host = "169.254.169.254") => Effect.acquireUseRelease( Effect.try(makeDirectFetch), (direct) => - HttpClient.get(url).pipe( + HttpClient.get(`http://${host}`).pipe( Effect.map((response) => response.headers["metadata-flavor"] === "Google"), Effect.provideService(FetchHttpClient.Fetch, direct.fetch), ), @@ -192,8 +191,6 @@ export const probeMetadataServer = (url: string) => Effect.orElseSucceed(() => false), ); -export const probeGoogleMetadataServer = probeMetadataServer("http://169.254.169.254"); - /** * `auth.type` names the selected method so a native logout clears only that method's credentials * instead of every stored token. Never holds a credential. From 147aba9d31379eb9ce45c9ea943b584683d02227 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Mon, 26 Oct 2026 16:02:20 +0000 Subject: [PATCH 6/6] fix(server): match the bundled runtime ADC directory --- .../provider/antigravityAuthSupport.test.ts | 18 ++++++++++++------ .../src/provider/antigravityAuthSupport.ts | 5 ++--- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/apps/server/src/provider/antigravityAuthSupport.test.ts b/apps/server/src/provider/antigravityAuthSupport.test.ts index 876a3db9d6336..88f37c904a3f9 100644 --- a/apps/server/src/provider/antigravityAuthSupport.test.ts +++ b/apps/server/src/provider/antigravityAuthSupport.test.ts @@ -677,10 +677,13 @@ it.layer(NodeServices.layer)("Antigravity Agent Platform credentials", (it) => { const sdkConfig = path.join(userHome, "sdk-config"); yield* fs.makeDirectory(sdkConfig); yield* fs.makeDirectory(path.join(userHome, ".config", "gcloud"), { recursive: true }); - yield* fs.writeFileString( - path.join(userHome, ".config", "gcloud", "application_default_credentials.json"), - "{}", + const adcPath = path.join( + userHome, + ".config", + "gcloud", + "application_default_credentials.json", ); + yield* fs.writeFileString(adcPath, "{}"); const auth: AntigravityAuthConfig = { authMethod: "agent-platform", apiKey: "", @@ -699,12 +702,13 @@ it.layer(NodeServices.layer)("Antigravity Agent Platform credentials", (it) => { onGoogleCloud: Effect.succeed(onGoogleCloud), }); - // CLOUDSDK_CONFIG replaces ~/.config/gcloud, as it does for gcloud. + // The bundled Go harness ignores CLOUDSDK_CONFIG when finding ADC. + expect(yield* issueWith({ CLOUDSDK_CONFIG: sdkConfig })).toBeNull(); + yield* fs.writeFileString(path.join(sdkConfig, "application_default_credentials.json"), "{}"); + yield* fs.remove(adcPath); expect(yield* issueWith({ CLOUDSDK_CONFIG: sdkConfig })).toContain( "gcloud auth application-default login", ); - yield* fs.writeFileString(path.join(sdkConfig, "application_default_credentials.json"), "{}"); - expect(yield* issueWith({ CLOUDSDK_CONFIG: sdkConfig })).toBeNull(); expect( yield* issueWith({ CLOUDSDK_CONFIG: path.join(userHome, "missing"), @@ -722,6 +726,7 @@ it.layer(NodeServices.layer)("Antigravity Agent Platform credentials", (it) => { ), ).toBeNull(); + yield* fs.writeFileString(adcPath, "{}"); // Windows reads %APPDATA%\gcloud and never the POSIX path. expect(yield* issueWith({ APPDATA: userHome }, "win32")).toContain( "gcloud auth application-default login", @@ -733,6 +738,7 @@ it.layer(NodeServices.layer)("Antigravity Agent Platform credentials", (it) => { ); expect(yield* issueWith({ APPDATA: userHome }, "win32")).toBeNull(); + yield* fs.remove(adcPath); // A metadata server supplies credentials without any local file. const missing = { CLOUDSDK_CONFIG: path.join(userHome, "missing") }; expect(yield* issueWith(missing, "linux")).toContain("gcloud auth application-default login"); diff --git a/apps/server/src/provider/antigravityAuthSupport.ts b/apps/server/src/provider/antigravityAuthSupport.ts index acc4f796c0c1b..08d500182d643 100644 --- a/apps/server/src/provider/antigravityAuthSupport.ts +++ b/apps/server/src/provider/antigravityAuthSupport.ts @@ -158,10 +158,9 @@ export const antigravityAuthIssue = Effect.fn("antigravityAuthIssue")(function* const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const gcloudDirectory = - host.environment.CLOUDSDK_CONFIG || - (host.platform === "win32" + host.platform === "win32" ? host.environment.APPDATA && path.join(host.environment.APPDATA, "gcloud") - : path.join(host.userHome, ".config", "gcloud")); + : path.join(host.userHome, ".config", "gcloud"); if ( gcloudDirectory && (yield* fs