diff --git a/apps/server/src/provider/AntigravityInstallation.test.ts b/apps/server/src/provider/AntigravityInstallation.test.ts index e2bab831712a..de72ebcc4e23 100644 --- a/apps/server/src/provider/AntigravityInstallation.test.ts +++ b/apps/server/src/provider/AntigravityInstallation.test.ts @@ -342,6 +342,8 @@ it.layer(NodeServices.layer)("Antigravity installation", (it) => { if (!profile) return yield* Effect.die("Expected a disposable validation profile."); profiles.add(profile); const helper = command.args[0] === "-e"; + // The runtime unpacks straight into the disposable profile. + if (!helper) expect(command.options.env?.TMPDIR).toBe(profile); const output = yield* Queue.unbounded(); const exited = yield* Deferred.make(); const terminate = Deferred.succeed(exited, ChildProcessSpawner.ExitCode(0)).pipe( diff --git a/apps/server/src/provider/AntigravityInstallation.ts b/apps/server/src/provider/AntigravityInstallation.ts index 24eb4e3d6df8..75baf453da03 100644 --- a/apps/server/src/provider/AntigravityInstallation.ts +++ b/apps/server/src/provider/AntigravityInstallation.ts @@ -476,6 +476,9 @@ export const makeAntigravityInstallation = Effect.fn("AntigravityInstallation.ma profileDirectory, platform, baseEnv: environment, + // The profile is scoped, so it cleans up the unpack; a shallow + // root keeps it under Windows' path limit. + tempDirectory: profileDirectory, }); const runtime = yield* makeAntigravityAcpRuntime({ spawn: buildAntigravityAcpSpawnInput({ diff --git a/apps/server/src/provider/Drivers/AntigravityDriver.test.ts b/apps/server/src/provider/Drivers/AntigravityDriver.test.ts index cc9ccb074b9e..18f7aaffee06 100644 --- a/apps/server/src/provider/Drivers/AntigravityDriver.test.ts +++ b/apps/server/src/provider/Drivers/AntigravityDriver.test.ts @@ -31,8 +31,7 @@ import { } from "../AntigravityInstallation.ts"; import { ANTIGRAVITY_AUTH_STDOUT_PREFIX, - resolveAntigravityProfileDirectory, - resolveAntigravityRuntimeTempDirectory, + resolveAntigravityInstanceDirectories, } from "../antigravityAuthSupport.ts"; import { NoOpProviderEventLoggers, ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts"; import * as ModelManifest from "../ModelManifest.ts"; @@ -74,7 +73,8 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* ( new URL("../../../scripts/acp-mock-agent.ts", import.meta.url), ); const requestLog = path.join(root, "requests.jsonl"); - const profileDirectory = resolveAntigravityProfileDirectory(config.stateDir, instanceId); + const directories = yield* resolveAntigravityInstanceDirectories(config.stateDir, instanceId); + const profileDirectory = directories.profile; const instancePath = `${path.join(root, "instance-bin")}:${baseEnv.PATH ?? ""}`; const makeExecutable = Effect.fn("AntigravityDriverTest.makeExecutable")(function* ( @@ -233,6 +233,7 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* ( fs, path, profileDirectory, + directories, instancePath, first, second, @@ -475,7 +476,7 @@ it.layer(testLayer)("AntigravityDriver", (it) => { () => Effect.gen(function* () { const h = yield* makeHarness(); - const tempRoot = resolveAntigravityRuntimeTempDirectory(h.profileDirectory); + const tempRoot = h.directories.runtimeTemp; yield* h.refresh(); yield* h.refresh(); const directories = h.launches.flatMap((launch) => @@ -498,12 +499,17 @@ it.layer(testLayer)("AntigravityDriver", (it) => { const path = yield* Path.Path; const config = yield* ServerConfig; const instanceId = ProviderInstanceId.make("antigravity-orphan-sweep"); - const tempRoot = resolveAntigravityRuntimeTempDirectory( - resolveAntigravityProfileDirectory(config.stateDir, instanceId), + const directories = yield* resolveAntigravityInstanceDirectories( + config.stateDir, + instanceId, ); - const orphan = path.join(tempRoot, "run-orphan", "_MEI123", "google3"); - yield* fs.makeDirectory(orphan, { recursive: true }); - yield* fs.writeFileString(path.join(orphan, "payload.bin"), "stale"); + // Older builds unpacked inside the profile. + const legacyRoot = path.join(directories.profile, "antigravity-acp", "tmp"); + for (const root of [directories.runtimeTemp, legacyRoot]) { + const orphan = path.join(root, "run-orphan", "_MEI123", "google3"); + yield* fs.makeDirectory(orphan, { recursive: true }); + yield* fs.writeFileString(path.join(orphan, "payload.bin"), "stale"); + } yield* AntigravityDriver.create({ instanceId, displayName: "Sweep", @@ -519,7 +525,8 @@ it.layer(testLayer)("AntigravityDriver", (it) => { }), ), ); - expect(yield* fs.exists(tempRoot)).toBe(false); + expect(yield* fs.exists(directories.runtimeTemp)).toBe(false); + expect(yield* fs.exists(legacyRoot)).toBe(false); }).pipe(Effect.scoped), ); diff --git a/apps/server/src/provider/Drivers/AntigravityDriver.ts b/apps/server/src/provider/Drivers/AntigravityDriver.ts index 1141ac5856fc..fb9c7041b5fd 100644 --- a/apps/server/src/provider/Drivers/AntigravityDriver.ts +++ b/apps/server/src/provider/Drivers/AntigravityDriver.ts @@ -33,8 +33,7 @@ import { buildAntigravityAcpSpawnInput, isAntigravitySignInRequiredError, prepareAntigravityProfile, - resolveAntigravityProfileDirectory, - resolveAntigravityRuntimeTempDirectory, + resolveAntigravityInstanceDirectories, type AntigravityAuthConfig, } from "../antigravityAuthSupport.ts"; import { @@ -103,15 +102,34 @@ export const AntigravityDriver: ProviderDriver + new ProviderDriverError({ + driver: DRIVER, + instanceId, + detail: "Could not resolve the Antigravity profile directory.", + cause, + }), + ), ); + const profileDirectory = directories.profile; // No process of this instance exists yet, so every runtime temp - // directory left under the profile is an orphan from a killed server. - yield* removeAntigravityRuntimeTempDirs( - resolveAntigravityRuntimeTempDirectory(profileDirectory), - ).pipe(Effect.provideService(FileSystem.FileSystem, fileSystem)); + // directory it owns is an orphan from a killed server. Older builds + // unpacked inside the profile. + for (const directory of [ + directories.runtimeTemp, + path.join(profileDirectory, "antigravity-acp", "tmp"), + ]) { + yield* removeAntigravityRuntimeTempDirs(directory).pipe( + Effect.provideService(FileSystem.FileSystem, fileSystem), + ); + } const continuationIdentity = defaultProviderContinuationIdentity({ driverKind: DRIVER, instanceId, @@ -165,6 +183,7 @@ export const AntigravityDriver: ProviderDriver { } }); - it("keeps accounts separate even when instance IDs differ only by case", () => { - const first = resolveAntigravityProfileDirectory( - "/userdata", - ProviderInstanceId.make("antigravity"), - ); - const second = resolveAntigravityProfileDirectory( - "/userdata", - ProviderInstanceId.make("Antigravity"), - ); - expect(first.toLowerCase()).not.toBe(second.toLowerCase()); - expect( - resolveAntigravityProfileDirectory("/userdata", ProviderInstanceId.make("antigravity")), - ).toBe(first); - }); + it.effect("keeps accounts separate even when instance IDs differ only by case", () => + Effect.gen(function* () { + const first = yield* resolveAntigravityInstanceDirectories( + "/userdata", + ProviderInstanceId.make("antigravity"), + ); + const second = yield* resolveAntigravityInstanceDirectories( + "/userdata", + ProviderInstanceId.make("Antigravity"), + ); + // Existing sign-ins live at this path; it must not move. + expect(first.profile).toBe( + "/userdata/providers/antigravity/ac0a3dfd6dddb20962cecff6ee5fe65e19d3923be20e52c5ab52ff877f7e4c32", + ); + expect(first.profile.toLowerCase()).not.toBe(second.profile.toLowerCase()); + expect(first.runtimeTemp.toLowerCase()).not.toBe(second.runtimeTemp.toLowerCase()); + }).pipe(Effect.provide(Layer.mergeAll(NodeCrypto.layer, NodePath.layerPosix))), + ); + + it.effect("keeps the unpacked Windows runtime under MAX_PATH for long user names", () => + Effect.gen(function* () { + const path = yield* Path.Path; + // Deepest member of the official agy_acp_server_1.1.1 windows-x86_64 bundle. + const deepestMember = + "google3\\cloud\\developer_experience\\antigravity_extensions\\acp_server\\_private__agy_acp_server_bin.lazy_imports_info.json"; + const directories = yield* resolveAntigravityInstanceDirectories( + "C:\\Users\\a-twenty-char-person\\.t3\\userdata", + ProviderInstanceId.make("antigravity"), + ); + const extracted = (tempDirectory: string) => + path.join(tempDirectory, "run-AbC123", "_MEI000012ab2", deepestMember); + // MAX_PATH is 260 including the terminating NUL. + expect(extracted(directories.runtimeTemp).length).toBeLessThan(260); + expect( + extracted(path.join(directories.profile, "antigravity-acp", "tmp")).length, + ).toBeGreaterThanOrEqual(260); + }).pipe(Effect.provide(Layer.mergeAll(NodeCrypto.layer, NodePath.layerWin32))), + ); }); describe("Antigravity authorization URL", () => { diff --git a/apps/server/src/provider/antigravityAuthSupport.ts b/apps/server/src/provider/antigravityAuthSupport.ts index b48368ad2a1d..fc7fcb0854cf 100644 --- a/apps/server/src/provider/antigravityAuthSupport.ts +++ b/apps/server/src/provider/antigravityAuthSupport.ts @@ -1,13 +1,12 @@ -import * as NodeCrypto from "node:crypto"; // @effect-diagnostics-next-line nodeBuiltinImport:off - Effect's symlink has no type argument, and Windows needs a junction to link without elevation. import * as NodeFSP from "node:fs/promises"; -// @effect-diagnostics-next-line nodeBuiltinImport:off - resolveAntigravityProfileDirectory is a pure sync helper, so it cannot use the Path service. -import * as NodePath from "node:path"; import type { AntigravityAuthMethod, ProviderInstanceId } from "@t3tools/contracts"; import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; import { resolveNodeExecutable, nodeRuntimeUnavailableMessage } from "@t3tools/shared/nodeRuntime"; +import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; +import * as Encoding from "effect/Encoding"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; import type * as PlatformError from "effect/PlatformError"; @@ -185,19 +184,32 @@ export function isAntigravitySignInRequiredError(error: unknown): boolean { ); } -/** Keeps case-sensitive instance IDs separate on case-insensitive filesystems. */ -export function resolveAntigravityProfileDirectory( - stateDir: string, - instanceId: ProviderInstanceId, -): string { - const directoryName = NodeCrypto.createHash("sha256").update(instanceId).digest("hex"); - return NodePath.join(stateDir, "providers", "antigravity", directoryName); +export interface AntigravityInstanceDirectories { + /** GEMINI_HOME for the agent. Holds the instance's Google sign-in. */ + readonly profile: string; + /** + * Parent of the per-process directories the agent unpacks into. It sits + * beside the profile, not inside it: the agent unpacks members up to 120 + * characters deep, and the profile's longer name would push them past + * Windows' 260-character path limit. + */ + readonly runtimeTemp: string; } -/** Parent of the per-process runtime temp directories inside a profile. */ -export function resolveAntigravityRuntimeTempDirectory(profileDirectory: string): string { - return NodePath.join(profileDirectory, "antigravity-acp", "tmp"); -} +/** Hashes the instance ID so case-only differences stay separate on case-insensitive filesystems. */ +export const resolveAntigravityInstanceDirectories = Effect.fn( + "resolveAntigravityInstanceDirectories", +)(function* (stateDir: string, instanceId: ProviderInstanceId) { + const crypto = yield* Crypto.Crypto; + const path = yield* Path.Path; + const key = Encoding.encodeHex( + yield* crypto.digest("SHA-256", new TextEncoder().encode(instanceId)), + ); + return { + profile: path.join(stateDir, "providers", "antigravity", key), + runtimeTemp: path.join(stateDir, "antigravity-tmp", key.slice(0, 12)), + } satisfies AntigravityInstanceDirectories; +}); function quoteBrowserArgument(value: string): string { return `'${value.replaceAll("'", `'"'"'`)}'`; @@ -300,6 +312,8 @@ export const prepareAntigravityProfile = Effect.fn("prepareAntigravityProfile")( readonly auth?: AntigravityAuthConfig; /** Home the agent expands `~` against. Defaults to the launch environment's. */ readonly userHome?: string; + /** Parent of per-process temp directories. Defaults to one inside the profile. */ + readonly tempDirectory?: string; }) { const auth = input.auth ?? ANTIGRAVITY_PERSONAL_AUTH; const fs = yield* FileSystem.FileSystem; @@ -337,7 +351,7 @@ export const prepareAntigravityProfile = Effect.fn("prepareAntigravityProfile")( const geminiHome = path.resolve(input.profileDirectory); const acpDirectory = path.join(geminiHome, "antigravity-acp"); - const tempDirectory = resolveAntigravityRuntimeTempDirectory(geminiHome); + const tempDirectory = input.tempDirectory ?? path.join(acpDirectory, "tmp"); const profile: AntigravityProfile = { platform, geminiHome,