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) => {
342342 if (!profile) return yield* Effect.die("Expected a disposable validation profile.");
343343 profiles.add(profile);
344344 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);
345347 const output = yield* Queue.unbounded<Uint8Array>();
346348 const exited = yield* Deferred.make<ChildProcessSpawner.ExitCode>();
347349 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
476476 profileDirectory,
477477 platform,
478478 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,
479482 });
480483 const runtime = yield* makeAntigravityAcpRuntime({
481484 spawn: buildAntigravityAcpSpawnInput({
apps/server/src/provider/Drivers/AntigravityDriver.test.ts+17-10
......@@ -31,8 +31,7 @@ import {
3131} from "../AntigravityInstallation.ts";
3232import {
3333 ANTIGRAVITY_AUTH_STDOUT_PREFIX,
34 resolveAntigravityProfileDirectory,
35 resolveAntigravityRuntimeTempDirectory,
34 resolveAntigravityInstanceDirectories,
3635} from "../antigravityAuthSupport.ts";
3736import { NoOpProviderEventLoggers, ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts";
3837import * as ModelManifest from "../ModelManifest.ts";
......@@ -74,7 +73,8 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* (
7473 new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),
7574 );
7675 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;
7878 const instancePath = `${path.join(root, "instance-bin")}:${baseEnv.PATH ?? ""}`;
7979
8080 const makeExecutable = Effect.fn("AntigravityDriverTest.makeExecutable")(function* (
......@@ -233,6 +233,7 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* (
233233 fs,
234234 path,
235235 profileDirectory,
236 directories,
236237 instancePath,
237238 first,
238239 second,
......@@ -475,7 +476,7 @@ it.layer(testLayer)("AntigravityDriver", (it) => {
475476 () =>
476477 Effect.gen(function* () {
477478 const h = yield* makeHarness();
478 const tempRoot = resolveAntigravityRuntimeTempDirectory(h.profileDirectory);
479 const tempRoot = h.directories.runtimeTemp;
479480 yield* h.refresh();
480481 yield* h.refresh();
481482 const directories = h.launches.flatMap((launch) =>
......@@ -498,12 +499,17 @@ it.layer(testLayer)("AntigravityDriver", (it) => {
498499 const path = yield* Path.Path;
499500 const config = yield* ServerConfig;
500501 const instanceId = ProviderInstanceId.make("antigravity-orphan-sweep");
501 const tempRoot = resolveAntigravityRuntimeTempDirectory(
502 resolveAntigravityProfileDirectory(config.stateDir, instanceId),
502 const directories = yield* resolveAntigravityInstanceDirectories(
503 config.stateDir,
504 instanceId,
503505 );
504 const orphan = path.join(tempRoot, "run-orphan", "_MEI123", "google3");
505 yield* fs.makeDirectory(orphan, { recursive: true });
506 yield* fs.writeFileString(path.join(orphan, "payload.bin"), "stale");
506 // Older builds unpacked inside the profile.
507 const legacyRoot = path.join(directories.profile, "antigravity-acp", "tmp");
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 }
507513 yield* AntigravityDriver.create({
508514 instanceId,
509515 displayName: "Sweep",
......@@ -519,7 +525,8 @@ it.layer(testLayer)("AntigravityDriver", (it) => {
519525 }),
520526 ),
521527 );
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);
523530 }).pipe(Effect.scoped),
524531 );
525532
apps/server/src/provider/Drivers/AntigravityDriver.ts+26-7
......@@ -33,8 +33,7 @@ import {
3333 buildAntigravityAcpSpawnInput,
3434 isAntigravitySignInRequiredError,
3535 prepareAntigravityProfile,
36 resolveAntigravityProfileDirectory,
37 resolveAntigravityRuntimeTempDirectory,
36 resolveAntigravityInstanceDirectories,
3837 type AntigravityAuthConfig,
3938} from "../antigravityAuthSupport.ts";
4039import {
......@@ -103,15 +102,34 @@ export const AntigravityDriver: ProviderDriver<AntigravitySettings, AntigravityD
103102 const authConfigIssue = antigravityAuthConfigIssue(auth);
104103 const processEnvironment = mergeProviderInstanceEnvironment(environment);
105104 const userHome = resolveAntigravityUserHome(yield* HostProcessPlatform, processEnvironment);
106 const profileDirectory = resolveAntigravityProfileDirectory(
105 const directories = yield* resolveAntigravityInstanceDirectories(
107106 serverConfig.stateDir,
108107 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 ),
109120 );
121 const profileDirectory = directories.profile;
110122 // No process of this instance exists yet, so every runtime temp
111 // directory left under the profile is an orphan from a killed server.
112 yield* removeAntigravityRuntimeTempDirs(
113 resolveAntigravityRuntimeTempDirectory(profileDirectory),
114 ).pipe(Effect.provideService(FileSystem.FileSystem, fileSystem));
123 // directory it owns is an orphan from a killed server. Older builds
124 // unpacked inside the profile.
125 for (const directory of [
126 directories.runtimeTemp,
127 path.join(profileDirectory, "antigravity-acp", "tmp"),
128 ]) {
129 yield* removeAntigravityRuntimeTempDirs(directory).pipe(
130 Effect.provideService(FileSystem.FileSystem, fileSystem),
131 );
132 }
115133 const continuationIdentity = defaultProviderContinuationIdentity({
116134 driverKind: DRIVER,
117135 instanceId,
......@@ -165,6 +183,7 @@ export const AntigravityDriver: ProviderDriver<AntigravitySettings, AntigravityD
165183 baseEnv: processEnvironment,
166184 auth,
167185 userHome,
186 tempDirectory: directories.runtimeTemp,
168187 }).pipe(
169188 Effect.provideService(FileSystem.FileSystem, fileSystem),
170189 Effect.provideService(Path.Path, path),
apps/server/src/provider/acp/AntigravitySessionFiles.ts+4-4
......@@ -43,10 +43,10 @@ export const removeAntigravitySessionFiles = Effect.fn("removeAntigravitySession
4343);
4444
4545/**
46 * Removes every per-process runtime temp directory under the profile. Call
47 * once when the driver starts, before it launches any process, so a previous
48 * server that was killed mid-session cannot leave unpacked runtimes behind.
49 * Only the profile-owned directory is touched. The system temp directory
46 * Removes every per-process runtime temp directory under an instance's root.
47 * Call once when the driver starts, before it launches any process, so a
48 * previous server that was killed mid-session cannot leave unpacked runtimes
49 * behind. Only T3-owned directories are touched. The system temp directory
5050 * belongs to other programs and Windows does not lock data files, so sweeping
5151 * it could gut a live extraction.
5252 */
apps/server/src/provider/antigravityAuthSupport.test.ts+42-15
......@@ -1,6 +1,8 @@
11// @effect-diagnostics-next-line nodeBuiltinImport:off
22import * as NodeChildProcess from "node:child_process";
33
4import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
5import * as NodePath from "@effect/platform-node/NodePath";
46import * as NodeServices from "@effect/platform-node/NodeServices";
57import { ProviderInstanceId } from "@t3tools/contracts";
68import {
......@@ -11,6 +13,7 @@ import {
1113import { describe, expect, it } from "@effect/vitest";
1214import * as Effect from "effect/Effect";
1315import * as FileSystem from "effect/FileSystem";
16import * as Layer from "effect/Layer";
1417import * as Path from "effect/Path";
1518import * as Result from "effect/Result";
1619import * as Schema from "effect/Schema";
......@@ -36,7 +39,7 @@ import {
3639 makeAntigravityStdoutTransform,
3740 parseAntigravityAuthorizationUrl,
3841 prepareAntigravityProfile,
39 resolveAntigravityProfileDirectory,
42 resolveAntigravityInstanceDirectories,
4043} from "./antigravityAuthSupport.ts";
4144
4245const authorizationUrl =
......@@ -246,20 +249,44 @@ describe("Antigravity process environment", () => {
246249 }
247250 });
248251
249 it("keeps accounts separate even when instance IDs differ only by case", () => {
250 const first = resolveAntigravityProfileDirectory(
251 "/userdata",
252 ProviderInstanceId.make("antigravity"),
253 );
254 const second = resolveAntigravityProfileDirectory(
255 "/userdata",
256 ProviderInstanceId.make("Antigravity"),
257 );
258 expect(first.toLowerCase()).not.toBe(second.toLowerCase());
259 expect(
260 resolveAntigravityProfileDirectory("/userdata", ProviderInstanceId.make("antigravity")),
261 ).toBe(first);
262 });
252 it.effect("keeps accounts separate even when instance IDs differ only by case", () =>
253 Effect.gen(function* () {
254 const first = yield* resolveAntigravityInstanceDirectories(
255 "/userdata",
256 ProviderInstanceId.make("antigravity"),
257 );
258 const second = yield* resolveAntigravityInstanceDirectories(
259 "/userdata",
260 ProviderInstanceId.make("Antigravity"),
261 );
262 // Existing sign-ins live at this path; it must not move.
263 expect(first.profile).toBe(
264 "/userdata/providers/antigravity/ac0a3dfd6dddb20962cecff6ee5fe65e19d3923be20e52c5ab52ff877f7e4c32",
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 );
263290});
264291
265292describe("Antigravity authorization URL", () => {
apps/server/src/provider/antigravityAuthSupport.ts+29-15
......@@ -1,13 +1,12 @@
1import * as NodeCrypto from "node:crypto";
21// @effect-diagnostics-next-line nodeBuiltinImport:off - Effect's symlink has no type argument, and Windows needs a junction to link without elevation.
32import * 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
74import type { AntigravityAuthMethod, ProviderInstanceId } from "@t3tools/contracts";
85import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
96import { resolveNodeExecutable, nodeRuntimeUnavailableMessage } from "@t3tools/shared/nodeRuntime";
7import * as Crypto from "effect/Crypto";
108import * as Effect from "effect/Effect";
9import * as Encoding from "effect/Encoding";
1110import * as FileSystem from "effect/FileSystem";
1211import * as Path from "effect/Path";
1312import type * as PlatformError from "effect/PlatformError";
......@@ -185,19 +184,32 @@ export function isAntigravitySignInRequiredError(error: unknown): boolean {
185184 );
186185}
187186
188/** Keeps case-sensitive instance IDs separate on case-insensitive filesystems. */
189export function resolveAntigravityProfileDirectory(
190 stateDir: string,
191 instanceId: ProviderInstanceId,
192): string {
193 const directoryName = NodeCrypto.createHash("sha256").update(instanceId).digest("hex");
194 return NodePath.join(stateDir, "providers", "antigravity", directoryName);
187export interface AntigravityInstanceDirectories {
188 /** GEMINI_HOME for the agent. Holds the instance's Google sign-in. */
189 readonly profile: string;
190 /**
191 * Parent of the per-process directories the agent unpacks into. It sits
192 * beside the profile, not inside it: the agent unpacks members up to 120
193 * characters deep, and the profile's longer name would push them past
194 * Windows' 260-character path limit.
195 */
196 readonly runtimeTemp: string;
195197}
196198
197/** Parent of the per-process runtime temp directories inside a profile. */
198export function resolveAntigravityRuntimeTempDirectory(profileDirectory: string): string {
199 return NodePath.join(profileDirectory, "antigravity-acp", "tmp");
200}
199/** Hashes the instance ID so case-only differences stay separate on case-insensitive filesystems. */
200export const resolveAntigravityInstanceDirectories = Effect.fn(
201 "resolveAntigravityInstanceDirectories",
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
202214function quoteBrowserArgument(value: string): string {
203215 return `'${value.replaceAll("'", `'"'"'`)}'`;
......@@ -300,6 +312,8 @@ export const prepareAntigravityProfile = Effect.fn("prepareAntigravityProfile")(
300312 readonly auth?: AntigravityAuthConfig;
301313 /** Home the agent expands `~` against. Defaults to the launch environment's. */
302314 readonly userHome?: string;
315 /** Parent of per-process temp directories. Defaults to one inside the profile. */
316 readonly tempDirectory?: string;
303317}) {
304318 const auth = input.auth ?? ANTIGRAVITY_PERSONAL_AUTH;
305319 const fs = yield* FileSystem.FileSystem;
......@@ -337,7 +351,7 @@ export const prepareAntigravityProfile = Effect.fn("prepareAntigravityProfile")(
337351
338352 const geminiHome = path.resolve(input.profileDirectory);
339353 const acpDirectory = path.join(geminiHome, "antigravity-acp");
340 const tempDirectory = resolveAntigravityRuntimeTempDirectory(geminiHome);
354 const tempDirectory = input.tempDirectory ?? path.join(acpDirectory, "tmp");
341355 const profile: AntigravityProfile = {
342356 platform,
343357 geminiHome,