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, },