From 8c18b5bb21a5349fbadba8c77456ea34c41b7339 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Tue, 15 Sep 2026 19:02:12 -0700 Subject: [PATCH] fix(antigravity): stop health checks from filling the disk with _MEI folders (#12008) The health probe launched the PyInstaller ACP binary every minute and force killed it, leaving about 1 GB of _MEI files per run. The probe now resolves the install on disk without spawning. Each ACP process gets its own temp directory under the profile that is removed when the runtime closes, and the driver sweeps the profile temp root on create. Continues #11657 by Vita Skacel. Owned temp directory approach from #9626 by ariszz. Co-authored-by: Vita Skacel Co-authored-by: Claude Fable 5.1 --- .../Drivers/AntigravityDriver.test.ts | 86 ++++++++++++++++- .../src/provider/Drivers/AntigravityDriver.ts | 95 +++++++++++++++---- .../provider/acp/AntigravitySessionFiles.ts | 18 ++++ .../provider/antigravityAuthSupport.test.ts | 43 +++++++++ .../src/provider/antigravityAuthSupport.ts | 22 ++++- 5 files changed, 244 insertions(+), 20 deletions(-) diff --git a/apps/server/src/provider/Drivers/AntigravityDriver.test.ts b/apps/server/src/provider/Drivers/AntigravityDriver.test.ts index 922a27df5768c0421562cf1b63380015d9567836..c740e69e60fa87068387033fa3e20618fb478ee5 100644 --- a/apps/server/src/provider/Drivers/AntigravityDriver.test.ts +++ b/apps/server/src/provider/Drivers/AntigravityDriver.test.ts @@ -31,6 +31,7 @@ import { import { ANTIGRAVITY_AUTH_STDOUT_PREFIX, resolveAntigravityProfileDirectory, + resolveAntigravityRuntimeTempDirectory, } from "../antigravityAuthSupport.ts"; import { NoOpProviderEventLoggers, ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts"; import * as ModelManifest from "../ModelManifest.ts"; @@ -58,7 +59,7 @@ function shellQuote(value: string): string { } const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* ( - options: { readonly config?: Partial } = {}, + options: { readonly config?: Partial; readonly enabled?: boolean } = {}, ) { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; @@ -124,11 +125,22 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* ( forceFileStorage: string | undefined; credentialKeys: ReadonlyArray; geminiApiKey: string | undefined; + tempDirectory: string | undefined; handle: ChildProcessSpawner.ChildProcessHandle; }> = []; const installation = Layer.mock(AntigravityInstallation)({ managedDirectory: root, + resolve: () => + Effect.gen(function* () { + if (controls.failResolution) { + return yield* new AntigravityInstallationError({ + operation: "resolve", + detail: "Fixture resolution failed.", + }); + } + return controls.selected; + }), acquire: (binaryPath, environment) => Effect.gen(function* () { acquisitions.push({ binaryPath, path: environment?.PATH }); @@ -166,6 +178,10 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* ( blockedCredentialKeys.has(key.toUpperCase()), ), geminiApiKey: environment.GEMINI_API_KEY, + // Only the agent gets a per-process temp directory. Other launches + // inherit the host TMPDIR. + tempDirectory: + environment.ANTIGRAVITY_HARNESS_PATH === undefined ? undefined : environment.TMPDIR, handle, }); return handle; @@ -174,7 +190,7 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* ( const instance = yield* AntigravityDriver.create({ instanceId, displayName: "Google test account", - enabled: false, + enabled: options.enabled ?? false, config: { ...AntigravityDriver.defaultConfig(), ...options.config }, environment: [ { name: "PATH", value: instancePath }, @@ -207,12 +223,14 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* ( yield* launch.handle.exitCode.pipe(Effect.ignore); expect(yield* launch.handle.isRunning).toBe(false); if (launch.cwd) expect(yield* fs.exists(launch.cwd)).toBe(false); + if (launch.tempDirectory) expect(yield* fs.exists(launch.tempDirectory)).toBe(false); } }); return { instance, refresh, fs, + path, profileDirectory, instancePath, first, @@ -435,4 +453,68 @@ it.layer(testLayer)("AntigravityDriver", (it) => { yield* h.assertClosed; }).pipe(Effect.scoped), ); + + it.effect.skipIf(windowsHost)( + "gives each process its own temp directory and removes it when the process closes", + () => + Effect.gen(function* () { + const h = yield* makeHarness(); + const tempRoot = resolveAntigravityRuntimeTempDirectory(h.profileDirectory); + yield* h.refresh(); + yield* h.refresh(); + const directories = h.launches.flatMap((launch) => + launch.tempDirectory === undefined ? [] : [launch.tempDirectory], + ); + expect(directories).toHaveLength(2); + for (const directory of directories) { + expect(h.path.dirname(directory)).toBe(tempRoot); + } + expect(new Set(directories).size).toBe(2); + yield* h.assertClosed; + }).pipe(Effect.scoped), + ); + + it.effect.skipIf(windowsHost)( + "removes runtime temp directories left by a previous server on create", + () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const config = yield* ServerConfig; + const instanceId = ProviderInstanceId.make("antigravity-orphan-sweep"); + const tempRoot = resolveAntigravityRuntimeTempDirectory( + resolveAntigravityProfileDirectory(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"); + yield* AntigravityDriver.create({ + instanceId, + displayName: "Sweep", + enabled: false, + config: AntigravityDriver.defaultConfig(), + environment: [], + }).pipe( + Effect.provide( + Layer.mock(AntigravityInstallation)({ + managedDirectory: config.stateDir, + resolve: () => Effect.die("unused"), + acquire: () => Effect.die("unused"), + }), + ), + ); + expect(yield* fs.exists(tempRoot)).toBe(false); + }).pipe(Effect.scoped), + ); + + it.effect("probes through installation resolution without launching a process", () => + Effect.gen(function* () { + const h = yield* makeHarness({ enabled: true }); + const snapshot = yield* h.instance.snapshot.refresh; + expect(snapshot.installed).toBe(true); + expect(snapshot.version).toBe(h.first.version); + expect(h.launches).toEqual([]); + expect(h.acquisitions).toEqual([]); + }).pipe(Effect.scoped), + ); }); diff --git a/apps/server/src/provider/Drivers/AntigravityDriver.ts b/apps/server/src/provider/Drivers/AntigravityDriver.ts index 0082f3cbdc30eb544dfefeeda11825b58c055f25..1887659db186cc3a14101126109cd0bde2947dfa 100644 --- a/apps/server/src/provider/Drivers/AntigravityDriver.ts +++ b/apps/server/src/provider/Drivers/AntigravityDriver.ts @@ -30,6 +30,7 @@ import { isAntigravitySignInRequiredError, prepareAntigravityProfile, resolveAntigravityProfileDirectory, + resolveAntigravityRuntimeTempDirectory, type AntigravityAuthConfig, } from "../antigravityAuthSupport.ts"; import { @@ -38,7 +39,10 @@ import { } from "../acp/AntigravityAcpSupport.ts"; import type { AcpSessionRuntime, AcpSessionRuntimeStartResult } from "../acp/AcpSessionRuntime.ts"; import type { ServerProviderDraft } from "../providerSnapshot.ts"; -import { removeAntigravitySessionFiles } from "../acp/AntigravitySessionFiles.ts"; +import { + removeAntigravityRuntimeTempDirs, + removeAntigravitySessionFiles, +} from "../acp/AntigravitySessionFiles.ts"; import { ProviderDriverError } from "../Errors.ts"; import { makeAntigravityAdapter } from "../Layers/AntigravityAdapter.ts"; import { makeAntigravityProvider } from "../Layers/AntigravityProvider.ts"; @@ -98,6 +102,11 @@ export const AntigravityDriver: ProviderDriver + new ProviderSetupError({ + instanceId, + operation: "start", + detail: "Could not create an Antigravity runtime temp directory.", + cause, + }), + ), + ), + (directory) => + fileSystem + .remove(directory, { recursive: true, force: true }) + .pipe( + Effect.catch(() => + Effect.logWarning("Could not remove an Antigravity runtime temp directory."), + ), + ), + ); const runtime = yield* makeAntigravityAcpRuntime({ ...input, authMethod: auth.authMethod, @@ -165,6 +201,7 @@ export const AntigravityDriver: ProviderDriver Scope.close(processScope, exit)); - return yield* authFlow - .withProcess( - Scope.close(processScope, Exit.void), - Effect.gen(function* () { - const runtime = yield* makeRuntime({ - cwd: serverConfig.stateDir, - clientInfo: { name: "t3-code-provider-probe", version: "0.0.0" }, - mcpServers: [], - }); - return yield* runtime.initialize(); - }), - ) - .pipe(Effect.provideService(Scope.Scope, processScope)); - }).pipe(Effect.scoped); + if (authConfigIssue !== null) { + return yield* new ProviderSetupError({ + instanceId, + operation: "configure", + detail: authConfigIssue, + }); + } + const executable = yield* installation + .resolve(settings.binaryPath, processEnvironment) + .pipe( + Effect.mapError( + (cause) => + new ProviderSetupError({ + instanceId, + operation: "resolve", + detail: cause.detail, + cause, + }), + ), + ); + return { + protocolVersion: 1, + agentCapabilities: { + loadSession: true, + promptCapabilities: { image: true, audio: true, embeddedContext: true }, + sessionCapabilities: { list: {}, resume: {} }, + }, + authMethods: [{ id: "oauth-personal", name: "Log in with Google" }], + agentInfo: { + name: "antigravity-acp", + title: "Google Antigravity", + version: executable.version ?? "unknown", + }, + }; + }); const provider = yield* makeAntigravityProvider(settings, { stampIdentity: classifyModels, diff --git a/apps/server/src/provider/acp/AntigravitySessionFiles.ts b/apps/server/src/provider/acp/AntigravitySessionFiles.ts index b00bffbc16468b95453d5413edcfaaa9eb4e11cf..07d66065d9ee2b7f1df2bb987cf0fa109b85e200 100644 --- a/apps/server/src/provider/acp/AntigravitySessionFiles.ts +++ b/apps/server/src/provider/acp/AntigravitySessionFiles.ts @@ -41,3 +41,21 @@ export const removeAntigravitySessionFiles = Effect.fn("removeAntigravitySession }, Effect.catch(() => Effect.logWarning("Could not remove temporary Antigravity session files.")), ); + +/** + * Removes every per-process runtime temp directory under the profile. Call + * once when the driver starts, before it launches any process, so a previous + * server that was killed mid-session cannot leave unpacked runtimes behind. + * Only the profile-owned directory is touched. The system temp directory + * belongs to other programs and Windows does not lock data files, so sweeping + * it could gut a live extraction. + */ +export const removeAntigravityRuntimeTempDirs = Effect.fn("removeAntigravityRuntimeTempDirs")( + function* (tempDirectory: string) { + const fs = yield* FileSystem.FileSystem; + yield* fs.remove(tempDirectory, { recursive: true, force: true }); + }, + Effect.catch(() => + Effect.logWarning("Could not remove leftover Antigravity runtime temp files."), + ), +); diff --git a/apps/server/src/provider/antigravityAuthSupport.test.ts b/apps/server/src/provider/antigravityAuthSupport.test.ts index 7f065caa4f6a948c6abaa0c0be1ea952561802bd..3c24da77b52fc4965bbbf5e0120918d2c006a7fb 100644 --- a/apps/server/src/provider/antigravityAuthSupport.test.ts +++ b/apps/server/src/provider/antigravityAuthSupport.test.ts @@ -51,6 +51,7 @@ describe("Antigravity process environment", () => { geminiHome: "/t3/userdata/providers/antigravity/profile", acpDirectory: "/t3/userdata/providers/antigravity/profile/antigravity-acp", tokenPath: "/t3/userdata/providers/antigravity/profile/antigravity-acp/acp_token.json", + tempDirectory: "/t3/userdata/providers/antigravity/profile/antigravity-acp/tmp", browserCommand: "managed-browser-helper", }; @@ -101,6 +102,7 @@ describe("Antigravity process environment", () => { BROWSER: profile.browserCommand, PYTHONUNBUFFERED: "1", ELECTRON_RUN_AS_NODE: "1", + TMPDIR: profile.tempDirectory, }, }); }); @@ -187,6 +189,47 @@ describe("Antigravity process environment", () => { ).toBeNull(); }); + it("isolates TEMP and TMP to the profile directory on Windows", () => { + const windowsProfile: AntigravityProfile = { + platform: "win32", + geminiHome: "C:\\state\\providers\\antigravity\\profile", + acpDirectory: "C:\\state\\providers\\antigravity\\profile\\antigravity-acp", + tokenPath: "C:\\state\\providers\\antigravity\\profile\\antigravity-acp\\acp_token.json", + tempDirectory: "C:\\state\\providers\\antigravity\\profile\\antigravity-acp\\tmp", + browserCommand: "managed-browser-helper", + }; + const input = { + installation: { + executablePath: "C:\\release\\agy_acp_server.exe", + harnessPath: "C:\\release\\localharness_external.exe", + }, + profile: windowsProfile, + cwd: "C:\\project", + baseEnv: { PATH: "C:\\Windows\\system32", TEMP: "C:\\Users\\user\\AppData\\Local\\Temp" }, + }; + const shared = buildAntigravityAcpSpawnInput(input); + expect(shared.env?.TEMP).toBe(windowsProfile.tempDirectory); + expect(shared.env?.TMP).toBe(windowsProfile.tempDirectory); + const perRun = buildAntigravityAcpSpawnInput({ + ...input, + runtimeTempDirectory: `${windowsProfile.tempDirectory}\\run-1`, + }); + expect(perRun.env?.TEMP).toBe(`${windowsProfile.tempDirectory}\\run-1`); + expect(perRun.env?.TMP).toBe(`${windowsProfile.tempDirectory}\\run-1`); + }); + + it("isolates TMPDIR to the profile directory on POSIX hosts", () => { + const spawn = buildAntigravityAcpSpawnInput({ + installation: { executablePath: "/release/acp", harnessPath: "/release/harness" }, + profile, + cwd: "/project", + baseEnv: { TMPDIR: "/tmp" }, + runtimeTempDirectory: `${profile.tempDirectory}/run-1`, + }); + expect(spawn.env?.TMPDIR).toBe(`${profile.tempDirectory}/run-1`); + expect(spawn.env?.TEMP).toBeUndefined(); + }); + it("uses the registry launch arguments for each supported host platform", () => { for (const platform of ["linux", "darwin", "win32"] as const) { const spawn = buildAntigravityAcpSpawnInput({ diff --git a/apps/server/src/provider/antigravityAuthSupport.ts b/apps/server/src/provider/antigravityAuthSupport.ts index 8e0040ae0c3efd09f823c5cdc92284b16cdc667d..4f42c46f73a187f460c910165abe9b130084503f 100644 --- a/apps/server/src/provider/antigravityAuthSupport.ts +++ b/apps/server/src/provider/antigravityAuthSupport.ts @@ -85,6 +85,8 @@ export interface AntigravityProfile { readonly geminiHome: string; readonly acpDirectory: string; readonly tokenPath: string; + /** Parent of the per-process temp directories PyInstaller unpacks into. */ + readonly tempDirectory: string; readonly browserCommand: string; } @@ -191,6 +193,11 @@ export function resolveAntigravityProfileDirectory( return NodePath.join(stateDir, "providers", "antigravity", directoryName); } +/** Parent of the per-process runtime temp directories inside a profile. */ +export function resolveAntigravityRuntimeTempDirectory(profileDirectory: string): string { + return NodePath.join(profileDirectory, "antigravity-acp", "tmp"); +} + function quoteBrowserArgument(value: string): string { return `'${value.replaceAll("'", `'"'"'`)}'`; } @@ -199,6 +206,7 @@ function antigravityEnvironment( profile: AntigravityProfile, baseEnv: NodeJS.ProcessEnv, auth: AntigravityAuthConfig, + runtimeTempDirectory?: string, ) { const environment: NodeJS.ProcessEnv = {}; for (const [key, value] of Object.entries(baseEnv)) { @@ -214,6 +222,10 @@ function antigravityEnvironment( : auth.authMethod === "agent-platform" && auth.apiKey ? { GOOGLE_API_KEY: auth.apiKey } : {}; + // The agent is a PyInstaller one-file bundle. It unpacks about 1 GB into + // the system temp directory per launch and a force kill leaves that behind. + // Point it at a T3-owned directory so the driver can reclaim the space. + const tempDirectory = runtimeTempDirectory ?? profile.tempDirectory; return { ...environment, ...credential, @@ -222,6 +234,9 @@ function antigravityEnvironment( BROWSER: profile.browserCommand, PYTHONUNBUFFERED: "1", ELECTRON_RUN_AS_NODE: "1", + ...(profile.platform === "win32" + ? { TEMP: tempDirectory, TMP: tempDirectory } + : { TMPDIR: tempDirectory }), }; } @@ -311,11 +326,13 @@ export const prepareAntigravityProfile = Effect.fn("prepareAntigravityProfile")( const geminiHome = path.resolve(input.profileDirectory); const acpDirectory = path.join(geminiHome, "antigravity-acp"); + const tempDirectory = resolveAntigravityRuntimeTempDirectory(geminiHome); const profile: AntigravityProfile = { platform, geminiHome, acpDirectory, tokenPath: path.join(acpDirectory, "acp_token.json"), + tempDirectory, browserCommand, }; const environment = antigravityEnvironment(profile, input.baseEnv ?? process.env, auth); @@ -358,7 +375,7 @@ export const prepareAntigravityProfile = Effect.fn("prepareAntigravityProfile")( ), ); - for (const directory of [geminiHome, acpDirectory]) { + for (const directory of [geminiHome, acpDirectory, tempDirectory]) { yield* fs .makeDirectory(directory, { recursive: true, mode: 0o700 }) .pipe( @@ -400,6 +417,8 @@ export function buildAntigravityAcpSpawnInput(input: { readonly cwd: string; readonly baseEnv?: NodeJS.ProcessEnv; readonly auth?: AntigravityAuthConfig; + /** Per-process temp directory. Defaults to the profile's shared temp directory. */ + readonly runtimeTempDirectory?: string; }): AcpSpawnInput { return { command: input.installation.executablePath, @@ -410,6 +429,7 @@ export function buildAntigravityAcpSpawnInput(input: { input.profile, input.baseEnv ?? process.env, input.auth ?? ANTIGRAVITY_PERSONAL_AUTH, + input.runtimeTempDirectory, ), ANTIGRAVITY_HARNESS_PATH: input.installation.harnessPath, }, -- 2.54.0