authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-24 13:35:02-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-24 13:35:02-07:00
logf3cb2a1fee65a55a4083c4cfe9317ca0d1e7c1d5
treef7e5dd7a6cbbb757d0ea7200dee33d4902913208
parent66129c6fd518aa01d873ebb47060b41c17312d60
signature Signed by PGP key B5690EEEBB952194

fix(antigravity): keep Windows runtime unpacking under MAX_PATH (#13389)

Co-authored-by: javiergusart <42075376+javiergusart@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

7 files changed, 123 insertions(+), 51 deletions(-)

apps/server/src/provider/AntigravityInstallation.test.ts+2
...@@ -342,6 +342,8 @@ it.layer(NodeServices.layer)("Antigravity installation", (it) => {...@@ -342,6 +342,8 @@ it.layer(NodeServices.layer)("Antigravity installation", (it) => {
342 if (!profile) return yield* Effect.die("Expected a disposable validation profile.");342 if (!profile) return yield* Effect.die("Expected a disposable validation profile.");
343 profiles.add(profile);343 profiles.add(profile);
344 const helper = command.args[0] === "-e";344 const helper = command.args[0] === "-e";
345 // The runtime unpacks straight into the disposable profile.
346 if (!helper) expect(command.options.env?.TMPDIR).toBe(profile);
345 const output = yield* Queue.unbounded<Uint8Array>();347 const output = yield* Queue.unbounded<Uint8Array>();
346 const exited = yield* Deferred.make<ChildProcessSpawner.ExitCode>();348 const exited = yield* Deferred.make<ChildProcessSpawner.ExitCode>();
347 const terminate = Deferred.succeed(exited, ChildProcessSpawner.ExitCode(0)).pipe(349 const terminate = Deferred.succeed(exited, ChildProcessSpawner.ExitCode(0)).pipe(
apps/server/src/provider/AntigravityInstallation.ts+3
...@@ -476,6 +476,9 @@ export const makeAntigravityInstallation = Effect.fn("AntigravityInstallation.ma...@@ -476,6 +476,9 @@ export const makeAntigravityInstallation = Effect.fn("AntigravityInstallation.ma
476 profileDirectory,476 profileDirectory,
477 platform,477 platform,
478 baseEnv: environment,478 baseEnv: environment,
479 // The profile is scoped, so it cleans up the unpack; a shallow
480 // root keeps it under Windows' path limit.
481 tempDirectory: profileDirectory,
479 });482 });
480 const runtime = yield* makeAntigravityAcpRuntime({483 const runtime = yield* makeAntigravityAcpRuntime({
481 spawn: buildAntigravityAcpSpawnInput({484 spawn: buildAntigravityAcpSpawnInput({
apps/server/src/provider/Drivers/AntigravityDriver.test.ts+17-10
...@@ -31,8 +31,7 @@ import {...@@ -31,8 +31,7 @@ import {
31} from "../AntigravityInstallation.ts";31} from "../AntigravityInstallation.ts";
32import {32import {
33 ANTIGRAVITY_AUTH_STDOUT_PREFIX,33 ANTIGRAVITY_AUTH_STDOUT_PREFIX,
34 resolveAntigravityProfileDirectory,34 resolveAntigravityInstanceDirectories,
35 resolveAntigravityRuntimeTempDirectory,
36} from "../antigravityAuthSupport.ts";35} from "../antigravityAuthSupport.ts";
37import { NoOpProviderEventLoggers, ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts";36import { NoOpProviderEventLoggers, ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts";
38import * as ModelManifest from "../ModelManifest.ts";37import * as ModelManifest from "../ModelManifest.ts";
...@@ -74,7 +73,8 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* (...@@ -74,7 +73,8 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* (
74 new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),73 new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),
75 );74 );
76 const requestLog = path.join(root, "requests.jsonl");75 const requestLog = path.join(root, "requests.jsonl");
77 const profileDirectory = resolveAntigravityProfileDirectory(config.stateDir, instanceId);76 const directories = yield* resolveAntigravityInstanceDirectories(config.stateDir, instanceId);
77 const profileDirectory = directories.profile;
78 const instancePath = `${path.join(root, "instance-bin")}:${baseEnv.PATH ?? ""}`;78 const instancePath = `${path.join(root, "instance-bin")}:${baseEnv.PATH ?? ""}`;
7979
80 const makeExecutable = Effect.fn("AntigravityDriverTest.makeExecutable")(function* (80 const makeExecutable = Effect.fn("AntigravityDriverTest.makeExecutable")(function* (
...@@ -233,6 +233,7 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* (...@@ -233,6 +233,7 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* (
233 fs,233 fs,
234 path,234 path,
235 profileDirectory,235 profileDirectory,
236 directories,
236 instancePath,237 instancePath,
237 first,238 first,
238 second,239 second,
...@@ -475,7 +476,7 @@ it.layer(testLayer)("AntigravityDriver", (it) => {...@@ -475,7 +476,7 @@ it.layer(testLayer)("AntigravityDriver", (it) => {
475 () =>476 () =>
476 Effect.gen(function* () {477 Effect.gen(function* () {
477 const h = yield* makeHarness();478 const h = yield* makeHarness();
478 const tempRoot = resolveAntigravityRuntimeTempDirectory(h.profileDirectory);479 const tempRoot = h.directories.runtimeTemp;
479 yield* h.refresh();480 yield* h.refresh();
480 yield* h.refresh();481 yield* h.refresh();
481 const directories = h.launches.flatMap((launch) =>482 const directories = h.launches.flatMap((launch) =>
...@@ -498,12 +499,17 @@ it.layer(testLayer)("AntigravityDriver", (it) => {...@@ -498,12 +499,17 @@ it.layer(testLayer)("AntigravityDriver", (it) => {
498 const path = yield* Path.Path;499 const path = yield* Path.Path;
499 const config = yield* ServerConfig;500 const config = yield* ServerConfig;
500 const instanceId = ProviderInstanceId.make("antigravity-orphan-sweep");501 const instanceId = ProviderInstanceId.make("antigravity-orphan-sweep");
501 const tempRoot = resolveAntigravityRuntimeTempDirectory(502 const directories = yield* resolveAntigravityInstanceDirectories(
502 resolveAntigravityProfileDirectory(config.stateDir, instanceId),503 config.stateDir,
504 instanceId,
503 );505 );
504 const orphan = path.join(tempRoot, "run-orphan", "_MEI123", "google3");506 // Older builds unpacked inside the profile.
505 yield* fs.makeDirectory(orphan, { recursive: true });507 const legacyRoot = path.join(directories.profile, "antigravity-acp", "tmp");
506 yield* fs.writeFileString(path.join(orphan, "payload.bin"), "stale");508 for (const root of [directories.runtimeTemp, legacyRoot]) {
509 const orphan = path.join(root, "run-orphan", "_MEI123", "google3");
510 yield* fs.makeDirectory(orphan, { recursive: true });
511 yield* fs.writeFileString(path.join(orphan, "payload.bin"), "stale");
512 }
507 yield* AntigravityDriver.create({513 yield* AntigravityDriver.create({
508 instanceId,514 instanceId,
509 displayName: "Sweep",515 displayName: "Sweep",
...@@ -519,7 +525,8 @@ it.layer(testLayer)("AntigravityDriver", (it) => {...@@ -519,7 +525,8 @@ it.layer(testLayer)("AntigravityDriver", (it) => {
519 }),525 }),
520 ),526 ),
521 );527 );
522 expect(yield* fs.exists(tempRoot)).toBe(false);528 expect(yield* fs.exists(directories.runtimeTemp)).toBe(false);
529 expect(yield* fs.exists(legacyRoot)).toBe(false);
523 }).pipe(Effect.scoped),530 }).pipe(Effect.scoped),
524 );531 );
525532
apps/server/src/provider/Drivers/AntigravityDriver.ts+26-7
...@@ -33,8 +33,7 @@ import {...@@ -33,8 +33,7 @@ import {
33 buildAntigravityAcpSpawnInput,33 buildAntigravityAcpSpawnInput,
34 isAntigravitySignInRequiredError,34 isAntigravitySignInRequiredError,
35 prepareAntigravityProfile,35 prepareAntigravityProfile,
36 resolveAntigravityProfileDirectory,36 resolveAntigravityInstanceDirectories,
37 resolveAntigravityRuntimeTempDirectory,
38 type AntigravityAuthConfig,37 type AntigravityAuthConfig,
39} from "../antigravityAuthSupport.ts";38} from "../antigravityAuthSupport.ts";
40import {39import {
...@@ -103,15 +102,34 @@ export const AntigravityDriver: ProviderDriver<AntigravitySettings, AntigravityD...@@ -103,15 +102,34 @@ export const AntigravityDriver: ProviderDriver<AntigravitySettings, AntigravityD
103 const authConfigIssue = antigravityAuthConfigIssue(auth);102 const authConfigIssue = antigravityAuthConfigIssue(auth);
104 const processEnvironment = mergeProviderInstanceEnvironment(environment);103 const processEnvironment = mergeProviderInstanceEnvironment(environment);
105 const userHome = resolveAntigravityUserHome(yield* HostProcessPlatform, processEnvironment);104 const userHome = resolveAntigravityUserHome(yield* HostProcessPlatform, processEnvironment);
106 const profileDirectory = resolveAntigravityProfileDirectory(105 const directories = yield* resolveAntigravityInstanceDirectories(
107 serverConfig.stateDir,106 serverConfig.stateDir,
108 instanceId,107 instanceId,
108 ).pipe(
109 Effect.provideService(Crypto.Crypto, crypto),
110 Effect.provideService(Path.Path, path),
111 Effect.mapError(
112 (cause) =>
113 new ProviderDriverError({
114 driver: DRIVER,
115 instanceId,
116 detail: "Could not resolve the Antigravity profile directory.",
117 cause,
118 }),
119 ),
109 );120 );
121 const profileDirectory = directories.profile;
110 // No process of this instance exists yet, so every runtime temp122 // No process of this instance exists yet, so every runtime temp
111 // directory left under the profile is an orphan from a killed server.123 // directory it owns is an orphan from a killed server. Older builds
112 yield* removeAntigravityRuntimeTempDirs(124 // unpacked inside the profile.
113 resolveAntigravityRuntimeTempDirectory(profileDirectory),125 for (const directory of [
114 ).pipe(Effect.provideService(FileSystem.FileSystem, fileSystem));126 directories.runtimeTemp,
127 path.join(profileDirectory, "antigravity-acp", "tmp"),
128 ]) {
129 yield* removeAntigravityRuntimeTempDirs(directory).pipe(
130 Effect.provideService(FileSystem.FileSystem, fileSystem),
131 );
132 }
115 const continuationIdentity = defaultProviderContinuationIdentity({133 const continuationIdentity = defaultProviderContinuationIdentity({
116 driverKind: DRIVER,134 driverKind: DRIVER,
117 instanceId,135 instanceId,
...@@ -165,6 +183,7 @@ export const AntigravityDriver: ProviderDriver<AntigravitySettings, AntigravityD...@@ -165,6 +183,7 @@ export const AntigravityDriver: ProviderDriver<AntigravitySettings, AntigravityD
165 baseEnv: processEnvironment,183 baseEnv: processEnvironment,
166 auth,184 auth,
167 userHome,185 userHome,
186 tempDirectory: directories.runtimeTemp,
168 }).pipe(187 }).pipe(
169 Effect.provideService(FileSystem.FileSystem, fileSystem),188 Effect.provideService(FileSystem.FileSystem, fileSystem),
170 Effect.provideService(Path.Path, path),189 Effect.provideService(Path.Path, path),
apps/server/src/provider/acp/AntigravitySessionFiles.ts+4-4
...@@ -43,10 +43,10 @@ export const removeAntigravitySessionFiles = Effect.fn("removeAntigravitySession...@@ -43,10 +43,10 @@ export const removeAntigravitySessionFiles = Effect.fn("removeAntigravitySession
43);43);
4444
45/**45/**
46 * Removes every per-process runtime temp directory under the profile. Call46 * Removes every per-process runtime temp directory under an instance's root.
47 * once when the driver starts, before it launches any process, so a previous47 * Call once when the driver starts, before it launches any process, so a
48 * server that was killed mid-session cannot leave unpacked runtimes behind.48 * previous server that was killed mid-session cannot leave unpacked runtimes
49 * Only the profile-owned directory is touched. The system temp directory49 * behind. Only T3-owned directories are touched. The system temp directory
50 * belongs to other programs and Windows does not lock data files, so sweeping50 * belongs to other programs and Windows does not lock data files, so sweeping
51 * it could gut a live extraction.51 * it could gut a live extraction.
52 */52 */
apps/server/src/provider/antigravityAuthSupport.test.ts+42-15
...@@ -1,6 +1,8 @@...@@ -1,6 +1,8 @@
1// @effect-diagnostics-next-line nodeBuiltinImport:off1// @effect-diagnostics-next-line nodeBuiltinImport:off
2import * as NodeChildProcess from "node:child_process";2import * as NodeChildProcess from "node:child_process";
33
4import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
5import * as NodePath from "@effect/platform-node/NodePath";
4import * as NodeServices from "@effect/platform-node/NodeServices";6import * as NodeServices from "@effect/platform-node/NodeServices";
5import { ProviderInstanceId } from "@t3tools/contracts";7import { ProviderInstanceId } from "@t3tools/contracts";
6import {8import {
...@@ -11,6 +13,7 @@ import {...@@ -11,6 +13,7 @@ import {
11import { describe, expect, it } from "@effect/vitest";13import { describe, expect, it } from "@effect/vitest";
12import * as Effect from "effect/Effect";14import * as Effect from "effect/Effect";
13import * as FileSystem from "effect/FileSystem";15import * as FileSystem from "effect/FileSystem";
16import * as Layer from "effect/Layer";
14import * as Path from "effect/Path";17import * as Path from "effect/Path";
15import * as Result from "effect/Result";18import * as Result from "effect/Result";
16import * as Schema from "effect/Schema";19import * as Schema from "effect/Schema";
...@@ -36,7 +39,7 @@ import {...@@ -36,7 +39,7 @@ import {
36 makeAntigravityStdoutTransform,39 makeAntigravityStdoutTransform,
37 parseAntigravityAuthorizationUrl,40 parseAntigravityAuthorizationUrl,
38 prepareAntigravityProfile,41 prepareAntigravityProfile,
39 resolveAntigravityProfileDirectory,42 resolveAntigravityInstanceDirectories,
40} from "./antigravityAuthSupport.ts";43} from "./antigravityAuthSupport.ts";
4144
42const authorizationUrl =45const authorizationUrl =
...@@ -246,20 +249,44 @@ describe("Antigravity process environment", () => {...@@ -246,20 +249,44 @@ describe("Antigravity process environment", () => {
246 }249 }
247 });250 });
248251
249 it("keeps accounts separate even when instance IDs differ only by case", () => {252 it.effect("keeps accounts separate even when instance IDs differ only by case", () =>
250 const first = resolveAntigravityProfileDirectory(253 Effect.gen(function* () {
251 "/userdata",254 const first = yield* resolveAntigravityInstanceDirectories(
252 ProviderInstanceId.make("antigravity"),255 "/userdata",
253 );256 ProviderInstanceId.make("antigravity"),
254 const second = resolveAntigravityProfileDirectory(257 );
255 "/userdata",258 const second = yield* resolveAntigravityInstanceDirectories(
256 ProviderInstanceId.make("Antigravity"),259 "/userdata",
257 );260 ProviderInstanceId.make("Antigravity"),
258 expect(first.toLowerCase()).not.toBe(second.toLowerCase());261 );
259 expect(262 // Existing sign-ins live at this path; it must not move.
260 resolveAntigravityProfileDirectory("/userdata", ProviderInstanceId.make("antigravity")),263 expect(first.profile).toBe(
261 ).toBe(first);264 "/userdata/providers/antigravity/ac0a3dfd6dddb20962cecff6ee5fe65e19d3923be20e52c5ab52ff877f7e4c32",
262 });265 );
266 expect(first.profile.toLowerCase()).not.toBe(second.profile.toLowerCase());
267 expect(first.runtimeTemp.toLowerCase()).not.toBe(second.runtimeTemp.toLowerCase());
268 }).pipe(Effect.provide(Layer.mergeAll(NodeCrypto.layer, NodePath.layerPosix))),
269 );
270
271 it.effect("keeps the unpacked Windows runtime under MAX_PATH for long user names", () =>
272 Effect.gen(function* () {
273 const path = yield* Path.Path;
274 // Deepest member of the official agy_acp_server_1.1.1 windows-x86_64 bundle.
275 const deepestMember =
276 "google3\\cloud\\developer_experience\\antigravity_extensions\\acp_server\\_private__agy_acp_server_bin.lazy_imports_info.json";
277 const directories = yield* resolveAntigravityInstanceDirectories(
278 "C:\\Users\\a-twenty-char-person\\.t3\\userdata",
279 ProviderInstanceId.make("antigravity"),
280 );
281 const extracted = (tempDirectory: string) =>
282 path.join(tempDirectory, "run-AbC123", "_MEI000012ab2", deepestMember);
283 // MAX_PATH is 260 including the terminating NUL.
284 expect(extracted(directories.runtimeTemp).length).toBeLessThan(260);
285 expect(
286 extracted(path.join(directories.profile, "antigravity-acp", "tmp")).length,
287 ).toBeGreaterThanOrEqual(260);
288 }).pipe(Effect.provide(Layer.mergeAll(NodeCrypto.layer, NodePath.layerWin32))),
289 );
263});290});
264291
265describe("Antigravity authorization URL", () => {292describe("Antigravity authorization URL", () => {
apps/server/src/provider/antigravityAuthSupport.ts+29-15
...@@ -1,13 +1,12 @@...@@ -1,13 +1,12 @@
1import * as NodeCrypto from "node:crypto";
2// @effect-diagnostics-next-line nodeBuiltinImport:off - Effect's symlink has no type argument, and Windows needs a junction to link without elevation.1// @effect-diagnostics-next-line nodeBuiltinImport:off - Effect's symlink has no type argument, and Windows needs a junction to link without elevation.
3import * as NodeFSP from "node:fs/promises";2import * as NodeFSP from "node:fs/promises";
4// @effect-diagnostics-next-line nodeBuiltinImport:off - resolveAntigravityProfileDirectory is a pure sync helper, so it cannot use the Path service.
5import * as NodePath from "node:path";
63
7import type { AntigravityAuthMethod, ProviderInstanceId } from "@t3tools/contracts";4import type { AntigravityAuthMethod, ProviderInstanceId } from "@t3tools/contracts";
8import { HostProcessPlatform } from "@t3tools/shared/hostProcess";5import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
9import { resolveNodeExecutable, nodeRuntimeUnavailableMessage } from "@t3tools/shared/nodeRuntime";6import { resolveNodeExecutable, nodeRuntimeUnavailableMessage } from "@t3tools/shared/nodeRuntime";
7import * as Crypto from "effect/Crypto";
10import * as Effect from "effect/Effect";8import * as Effect from "effect/Effect";
9import * as Encoding from "effect/Encoding";
11import * as FileSystem from "effect/FileSystem";10import * as FileSystem from "effect/FileSystem";
12import * as Path from "effect/Path";11import * as Path from "effect/Path";
13import type * as PlatformError from "effect/PlatformError";12import type * as PlatformError from "effect/PlatformError";
...@@ -185,19 +184,32 @@ export function isAntigravitySignInRequiredError(error: unknown): boolean {...@@ -185,19 +184,32 @@ export function isAntigravitySignInRequiredError(error: unknown): boolean {
185 );184 );
186}185}
187186
188/** Keeps case-sensitive instance IDs separate on case-insensitive filesystems. */187export interface AntigravityInstanceDirectories {
189export function resolveAntigravityProfileDirectory(188 /** GEMINI_HOME for the agent. Holds the instance's Google sign-in. */
190 stateDir: string,189 readonly profile: string;
191 instanceId: ProviderInstanceId,190 /**
192): string {191 * Parent of the per-process directories the agent unpacks into. It sits
193 const directoryName = NodeCrypto.createHash("sha256").update(instanceId).digest("hex");192 * beside the profile, not inside it: the agent unpacks members up to 120
194 return NodePath.join(stateDir, "providers", "antigravity", directoryName);193 * characters deep, and the profile's longer name would push them past
194 * Windows' 260-character path limit.
195 */
196 readonly runtimeTemp: string;
195}197}
196198
197/** Parent of the per-process runtime temp directories inside a profile. */199/** Hashes the instance ID so case-only differences stay separate on case-insensitive filesystems. */
198export function resolveAntigravityRuntimeTempDirectory(profileDirectory: string): string {200export const resolveAntigravityInstanceDirectories = Effect.fn(
199 return NodePath.join(profileDirectory, "antigravity-acp", "tmp");201 "resolveAntigravityInstanceDirectories",
200}202)(function* (stateDir: string, instanceId: ProviderInstanceId) {
203 const crypto = yield* Crypto.Crypto;
204 const path = yield* Path.Path;
205 const key = Encoding.encodeHex(
206 yield* crypto.digest("SHA-256", new TextEncoder().encode(instanceId)),
207 );
208 return {
209 profile: path.join(stateDir, "providers", "antigravity", key),
210 runtimeTemp: path.join(stateDir, "antigravity-tmp", key.slice(0, 12)),
211 } satisfies AntigravityInstanceDirectories;
212});
201213
202function quoteBrowserArgument(value: string): string {214function quoteBrowserArgument(value: string): string {
203 return `'${value.replaceAll("'", `'"'"'`)}'`;215 return `'${value.replaceAll("'", `'"'"'`)}'`;
...@@ -300,6 +312,8 @@ export const prepareAntigravityProfile = Effect.fn("prepareAntigravityProfile")(...@@ -300,6 +312,8 @@ export const prepareAntigravityProfile = Effect.fn("prepareAntigravityProfile")(
300 readonly auth?: AntigravityAuthConfig;312 readonly auth?: AntigravityAuthConfig;
301 /** Home the agent expands `~` against. Defaults to the launch environment's. */313 /** Home the agent expands `~` against. Defaults to the launch environment's. */
302 readonly userHome?: string;314 readonly userHome?: string;
315 /** Parent of per-process temp directories. Defaults to one inside the profile. */
316 readonly tempDirectory?: string;
303}) {317}) {
304 const auth = input.auth ?? ANTIGRAVITY_PERSONAL_AUTH;318 const auth = input.auth ?? ANTIGRAVITY_PERSONAL_AUTH;
305 const fs = yield* FileSystem.FileSystem;319 const fs = yield* FileSystem.FileSystem;
...@@ -337,7 +351,7 @@ export const prepareAntigravityProfile = Effect.fn("prepareAntigravityProfile")(...@@ -337,7 +351,7 @@ export const prepareAntigravityProfile = Effect.fn("prepareAntigravityProfile")(
337351
338 const geminiHome = path.resolve(input.profileDirectory);352 const geminiHome = path.resolve(input.profileDirectory);
339 const acpDirectory = path.join(geminiHome, "antigravity-acp");353 const acpDirectory = path.join(geminiHome, "antigravity-acp");
340 const tempDirectory = resolveAntigravityRuntimeTempDirectory(geminiHome);354 const tempDirectory = input.tempDirectory ?? path.join(acpDirectory, "tmp");
341 const profile: AntigravityProfile = {355 const profile: AntigravityProfile = {
342 platform,356 platform,
343 geminiHome,357 geminiHome,