1import * as ScriptProgress from "@t3tools/provider-core/server/ScriptProgress";
2import { withAgentDeviceEnvironment } from "@t3tools/provider-core/server/mcpSession";
3import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions";
4import { AntigravitySettings, ProviderDriverKind, ProviderSetupError } from "@t3tools/contracts";
5import * as HostProcess from "@t3tools/shared/HostProcess";
6import { resolveSelfInvocation } from "@t3tools/shared/nodeRuntime";
7import {
8 NodeRuntimeUnavailableError,
9 nodeRuntimeUnavailableMessage,
10} from "@t3tools/shared/nodeRuntime";
11import * as Crypto from "effect/Crypto";
12import * as Deferred from "effect/Deferred";
13import * as Effect from "effect/Effect";
14import * as Exit from "effect/Exit";
15import * as FileSystem from "effect/FileSystem";
16import * as Path from "effect/Path";
17import * as Schema from "effect/Schema";
18import * as Scope from "effect/Scope";
19import * as Stream from "effect/Stream";
20import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
21import { HttpClient } from "effect/http";
22import type { AcpError } from "effect-acp/errors";
23
24import * as ProviderHost from "@t3tools/provider-core/server/ProviderHost";
25import {
26 isAntigravityTextGenerationAvailable,
27 makeAntigravityTextGeneration,
28} from "../../textGeneration/AntigravityTextGeneration.ts";
29import { makeAntigravityAuth, type AntigravityAuth } from "../AntigravityAuth.ts";
30import * as AntigravityInstallation from "../AntigravityInstallation.ts";
31import * as AntigravityUsage from "./AntigravityUsage.ts";
32import {
33 antigravityAuthConfigIssue,
34 antigravityAuthLabel,
35 antigravityAuthUsesBrowser,
36 buildAntigravityAcpSpawnInput,
37 isAntigravitySignInRequiredError,
38 prepareAntigravityProfile,
39 resolveAntigravityInstanceDirectories,
40 type AntigravityAuthConfig,
41} from "../antigravityAuthSupport.ts";
42import {
43 makeAntigravityAcpRuntime,
44 type AntigravityAcpRuntimeInput,
45} from "../acp/AntigravityAcpSupport.ts";
46import type * as AcpSessionRuntime from "@t3tools/provider-acp/server/AcpSessionRuntime";
47import type { ServerProviderDraft } from "@t3tools/provider-core/server/snapshotProbe";
48import {
49 removeAntigravityRuntimeTempDirs,
50 removeAntigravitySessionFiles,
51} from "../acp/AntigravitySessionFiles.ts";
52import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
53import * as ProviderContinuationRequests from "@t3tools/provider-core/server/ProviderContinuationRequests";
54import { makeAntigravityAdapterV2 } from "../../orchestration-v2/Adapters/AntigravityAdapterV2.ts";
55import { makeAcpNativeLoggerFactory } from "@t3tools/provider-acp/server/nativeLogging";
56import { ProviderDriverError } from "../Errors.ts";
57import { makeAntigravityProvider } from "../AntigravityProvider.ts";
58import { makeAntigravityUsageProbe } from "../antigravityUsageLimits.ts";
59import * as ProviderEventLoggers from "@t3tools/provider-core/server/ProviderEventLoggers";
60import * as ModelCatalog from "@t3tools/provider-core/server/ModelCatalog";
61import {
62 defaultProviderContinuationIdentity,
63 type ProviderDriver,
64 type ProviderInstance,
65} from "@t3tools/provider-core/server/driver";
66import { mergeProviderInstanceEnvironment } from "@t3tools/provider-core/server/instanceEnvironment";
67import { withInstanceIdentity } from "@t3tools/provider-core/server/instanceIdentity";
68import { discoverAntigravitySkills, resolveAntigravityUserHome } from "./AntigravitySkills.ts";
69
70const DRIVER = ProviderDriverKind.make("antigravity");
71const decodeSettings = Schema.decodeSync(AntigravitySettings);
72const isNodeRuntimeUnavailableError = Schema.is(NodeRuntimeUnavailableError);
73
74export type AntigravityDriverEnv =
75 | AntigravityInstallation.AntigravityInstallation
76 | ProviderHost.ProviderHost
77 | ChildProcessSpawner.ChildProcessSpawner
78 | Crypto.Crypto
79 | FileSystem.FileSystem
80 | HttpClient.HttpClient
81 | IdAllocator.IdAllocatorV2
82 | ScriptProgress.ScriptProgress
83 | McpProviderSessions.McpProviderSessions
84 | ModelCatalog.ModelCatalog
85 | Path.Path
86 | ProviderEventLoggers.ProviderEventLoggers;
87
88/** Each instance owns its Google profile. Executable releases are shared by the environment. */
89export const AntigravityDriver: ProviderDriver<
90 AntigravitySettings,
91 AntigravityDriverEnv,
92 AntigravityUsage.AntigravityUsage
93> = {
94 driverKind: DRIVER,
95 metadata: { displayName: "Antigravity", supportsMultipleInstances: true },
96 configSchema: AntigravitySettings,
97 defaultConfig: () => decodeSettings({}),
98 usage: AntigravityUsage.antigravityUsageReader,
99 create: ({ instanceId, displayName, accentColor, environment, enabled, config }) =>
100 Effect.gen(function* () {
101 const crypto = yield* Crypto.Crypto;
102 const fileSystem = yield* FileSystem.FileSystem;
103 const httpClient = yield* HttpClient.HttpClient;
104 const path = yield* Path.Path;
105 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
106 const host = yield* ProviderHost.ProviderHost;
107 const selfInvocation = yield* resolveSelfInvocation();
108 const installation = yield* AntigravityInstallation.AntigravityInstallation;
109 const loggers = yield* ProviderEventLoggers.ProviderEventLoggers;
110 const modelCatalog = yield* ModelCatalog.ModelCatalog;
111 const currentCatalog = modelCatalog.current(DRIVER);
112 const continuationRequests = yield* ProviderContinuationRequests.ProviderContinuationRequests;
113 const makeNativeLogger = yield* makeAcpNativeLoggerFactory();
114 const settings = { ...config, enabled } satisfies AntigravitySettings;
115 const auth: AntigravityAuthConfig = {
116 authMethod: settings.authMethod,
117 apiKey: settings.apiKey,
118 gcpProject: settings.gcpProject,
119 gcpLocation: settings.gcpLocation,
120 };
121 const authConfigIssue = antigravityAuthConfigIssue(auth);
122 const processEnvironment = yield* mergeProviderInstanceEnvironment(environment);
123 const userHome = resolveAntigravityUserHome(
124 yield* HostProcess.Platform,
125 processEnvironment,
126 yield* HostProcess.HomeDirectory,
127 );
128 const directories = yield* resolveAntigravityInstanceDirectories(
129 host.paths.stateDir,
130 instanceId,
131 ).pipe(
132 Effect.provideService(Crypto.Crypto, crypto),
133 Effect.provideService(Path.Path, path),
134 Effect.mapError(
135 (cause) =>
136 new ProviderDriverError({
137 driver: DRIVER,
138 instanceId,
139 detail: "Could not resolve the Antigravity profile directory.",
140 cause,
141 }),
142 ),
143 );
144 const profileDirectory = directories.profile;
145 // No process of this instance exists yet, so every runtime temp
146 // directory it owns is an orphan from a killed server. Older builds
147 // unpacked inside the profile.
148 for (const directory of [
149 directories.runtimeTemp,
150 path.join(profileDirectory, "antigravity-acp", "tmp"),
151 ]) {
152 yield* removeAntigravityRuntimeTempDirs(directory).pipe(
153 Effect.provideService(FileSystem.FileSystem, fileSystem),
154 );
155 }
156 const continuationIdentity = defaultProviderContinuationIdentity({
157 driverKind: DRIVER,
158 instanceId,
159 });
160 const stampIdentity = withInstanceIdentity({
161 instanceId,
162 driverKind: DRIVER,
163 displayName,
164 accentColor,
165 continuationGroupKey: continuationIdentity.continuationKey,
166 });
167 const classifyModels = (draft: ServerProviderDraft) =>
168 currentCatalog.pipe(
169 Effect.map((catalog) => stampIdentity(ModelCatalog.applyModelCatalog(draft, catalog))),
170 );
171
172 const makeRuntime = Effect.fn("AntigravityDriver.makeRuntime")(function* (
173 input: Omit<AntigravityAcpRuntimeInput, "spawn" | "childProcessSpawner">,
174 ): Effect.fn.Return<
175 AcpSessionRuntime.AcpSessionRuntime["Service"],
176 AcpError | ProviderSetupError,
177 Scope.Scope
178 > {
179 if (authConfigIssue !== null) {
180 return yield* new ProviderSetupError({
181 instanceId,
182 operation: "configure",
183 detail: authConfigIssue,
184 });
185 }
186 const executable = yield* installation
187 .acquire(settings.binaryPath, processEnvironment)
188 .pipe(
189 Effect.mapError(
190 (cause) =>
191 new ProviderSetupError({
192 instanceId,
193 operation: "resolve",
194 detail: cause.detail,
195 cause,
196 }),
197 ),
198 );
199 const profile = yield* prepareAntigravityProfile({
200 profileDirectory,
201 baseEnv: processEnvironment,
202 auth,
203 userHome,
204 tempDirectory: directories.runtimeTemp,
205 }).pipe(
206 Effect.provideService(FileSystem.FileSystem, fileSystem),
207 Effect.provideService(Path.Path, path),
208 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
209 Effect.mapError((cause) =>
210 isNodeRuntimeUnavailableError(cause.cause)
211 ? new ProviderSetupError({
212 instanceId,
213 operation: "start",
214 detail: nodeRuntimeUnavailableMessage("Antigravity sign-in"),
215 cause,
216 })
217 : cause,
218 ),
219 );
220 // Each process unpacks into its own directory that dies with the
221 // runtime scope, after the child is killed. A shared directory would
222 // let one session's teardown delete files a sibling still reads.
223 // Removal is best effort: a handle can outlive the kill on Windows,
224 // and the sweep on the next driver start reclaims what is left.
225 const runtimeTempDirectory = yield* Effect.acquireRelease(
226 fileSystem.makeTempDirectory({ directory: profile.tempDirectory, prefix: "run-" }).pipe(
227 Effect.mapError(
228 (cause) =>
229 new ProviderSetupError({
230 instanceId,
231 operation: "start",
232 detail: "Could not create an Antigravity runtime temp directory.",
233 cause,
234 }),
235 ),
236 ),
237 (directory) =>
238 fileSystem
239 .remove(directory, { recursive: true, force: true })
240 .pipe(
241 Effect.catch(() =>
242 Effect.logWarning("Could not remove an Antigravity runtime temp directory."),
243 ),
244 ),
245 );
246 const runtime = yield* makeAntigravityAcpRuntime({
247 ...input,
248 authMethod: auth.authMethod,
249 childProcessSpawner: spawner,
250 spawn: buildAntigravityAcpSpawnInput({
251 installation: executable,
252 profile,
253 cwd: input.cwd,
254 baseEnv: ScriptProgress.withScriptProgressEnvironment(
255 withAgentDeviceEnvironment(processEnvironment, input),
256 input.processEnvironment ?? {},
257 ),
258 auth,
259 runtimeTempDirectory,
260 }),
261 }).pipe(Effect.provideService(Crypto.Crypto, crypto));
262 return {
263 ...runtime,
264 start: () =>
265 runtime
266 .start()
267 .pipe(
268 Effect.tapError((cause): Effect.Effect<void> =>
269 input.onAuthorizationUrl === undefined && isAntigravitySignInRequiredError(cause)
270 ? provider.onAuthRequired
271 : Effect.void,
272 ),
273 ),
274 };
275 });
276
277 const makeDisposableRuntime = Effect.fn("AntigravityDriver.makeDisposableRuntime")(function* (
278 input: Pick<AntigravityAcpRuntimeInput, "onAuthorizationUrl">,
279 ) {
280 const cwd = yield* fileSystem
281 .makeTempDirectoryScoped({ prefix: "t3-antigravity-setup-" })
282 .pipe(
283 Effect.mapError(
284 () =>
285 new ProviderSetupError({
286 instanceId,
287 operation: "start",
288 detail: "Could not create an Antigravity setup workspace.",
289 }),
290 ),
291 );
292 let sessionId: string | undefined;
293 yield* Effect.addFinalizer(() =>
294 removeAntigravitySessionFiles({
295 profileDirectory,
296 sessionId,
297 cwd,
298 }).pipe(
299 Effect.provideService(FileSystem.FileSystem, fileSystem),
300 Effect.provideService(Path.Path, path),
301 ),
302 );
303 const runtime = yield* makeRuntime({
304 cwd,
305 clientInfo: { name: "t3-code-provider-setup", version: "0.0.0" },
306 mcpServers: [],
307 ...(input.onAuthorizationUrl ? { onAuthorizationUrl: input.onAuthorizationUrl } : {}),
308 });
309 return {
310 ...runtime,
311 start: () =>
312 runtime.start().pipe(
313 Effect.tap((started) =>
314 Effect.sync(() => {
315 sessionId = started.sessionId;
316 }),
317 ),
318 ),
319 };
320 });
321
322 const publishCatalog = (
323 started: AcpSessionRuntime.AcpSessionRuntimeStartResult,
324 runtime: Pick<AcpSessionRuntime.AcpSessionRuntime["Service"], "getEvents" | "drainEvents">,
325 ): Effect.Effect<void> =>
326 Effect.gen(function* () {
327 yield* provider.onSessionStarted(started);
328 yield* Stream.runForEach(runtime.getEvents(), (event) => {
329 if (event._tag === "EventStreamBarrier") {
330 return Deferred.succeed(event.acknowledge, undefined).pipe(Effect.asVoid);
331 }
332 if (event._tag === "ConfigOptionsUpdated") {
333 return provider.onConfigOptionsUpdated(event.configOptions);
334 }
335 return event._tag === "AvailableCommandsUpdated"
336 ? provider.onAvailableCommands(event.availableCommands)
337 : Effect.void;
338 }).pipe(Effect.forkScoped);
339 yield* runtime.drainEvents;
340 }).pipe(Effect.scoped);
341
342 const authFlow: AntigravityAuth = yield* makeAntigravityAuth({
343 instanceId,
344 makeRuntime: makeDisposableRuntime,
345 onAuthenticated: publishCatalog,
346 onSignedOut: Effect.suspend(() => provider.onSignedOut),
347 usesBrowser: antigravityAuthUsesBrowser(auth.authMethod),
348 });
349
350 // Kick the TTL-gated manifest refresh alongside the health check, as
351 // Codex and Claude do. Without it an environment that only runs
352 // Antigravity would keep classifying against a stale disk cache.
353 // The probe must not spawn. The agent is a PyInstaller one-file bundle
354 // that unpacks about 1 GB per launch, and the health check runs every
355 // minute. Resolving the install on disk is enough to report installed
356 // and version. The response below is synthetic: only agentInfo.version
357 // is read from it. Sessions and manual refreshes still spawn.
358 const probe = Effect.gen(function* () {
359 yield* modelCatalog.refreshInBackground;
360 if (authConfigIssue !== null) {
361 return yield* new ProviderSetupError({
362 instanceId,
363 operation: "configure",
364 detail: authConfigIssue,
365 });
366 }
367 const executable = yield* installation
368 .resolve(settings.binaryPath, processEnvironment)
369 .pipe(
370 Effect.mapError(
371 (cause) =>
372 new ProviderSetupError({
373 instanceId,
374 operation: "resolve",
375 detail: cause.detail,
376 cause,
377 }),
378 ),
379 );
380 return {
381 protocolVersion: 1,
382 agentCapabilities: {
383 loadSession: true,
384 promptCapabilities: { image: true, audio: true, embeddedContext: true },
385 sessionCapabilities: { list: {}, resume: {} },
386 },
387 authMethods: [{ id: "oauth-personal", name: "Log in with Google" }],
388 agentInfo: {
389 name: "antigravity-acp",
390 title: "Google Antigravity",
391 version: executable.version ?? "unknown",
392 },
393 };
394 });
395
396 const usage = yield* makeAntigravityUsageProbe({
397 profileDirectory,
398 authMethod: auth.authMethod,
399 }).pipe(
400 Effect.provideService(FileSystem.FileSystem, fileSystem),
401 Effect.provideService(Path.Path, path),
402 Effect.provideService(Crypto.Crypto, crypto),
403 Effect.provideService(HttpClient.HttpClient, httpClient),
404 );
405 const provider = yield* makeAntigravityProvider(settings, {
406 stampIdentity: classifyModels,
407 probe,
408 probeUsage: usage.probe,
409 clearUsage: usage.clear,
410 auth: { type: auth.authMethod, label: antigravityAuthLabel(auth.authMethod) },
411 supportsTextGeneration: isAntigravityTextGenerationAvailable(profileDirectory).pipe(
412 Effect.provideService(FileSystem.FileSystem, fileSystem),
413 Effect.provideService(Path.Path, path),
414 Effect.orElseSucceed(() => false),
415 ),
416 }).pipe(
417 Effect.mapError(
418 (cause) =>
419 new ProviderDriverError({
420 driver: DRIVER,
421 instanceId,
422 detail: "Could not prepare the Antigravity provider status.",
423 cause,
424 }),
425 ),
426 );
427 const defaultModel = currentCatalog.pipe(Effect.map((catalog) => catalog?.defaultChatModel));
428 const orchestrationAdapter = yield* makeAntigravityAdapterV2({
429 instanceId,
430 selfInvocation,
431 makeRuntime,
432 withProcess: authFlow.withProcess,
433 defaultModel,
434 onSessionStarted: provider.onSessionStarted,
435 onSessionEvent: (event) => {
436 if (event._tag === "ConfigOptionsUpdated") {
437 return provider.onConfigOptionsUpdated(event.configOptions);
438 }
439 return event._tag === "AvailableCommandsUpdated"
440 ? provider.onAvailableCommands(event.availableCommands)
441 : Effect.void;
442 },
443 continuationRequests,
444 nativeLogging: (threadId) =>
445 makeNativeLogger({
446 nativeEventLogger: loggers.native,
447 provider: DRIVER,
448 threadId,
449 }),
450 });
451 const textGeneration = yield* makeAntigravityTextGeneration({
452 profileDirectory,
453 defaultModel,
454 withProcess: authFlow.withProcess,
455 makeRuntime: (cwd) =>
456 makeRuntime({
457 cwd,
458 clientInfo: { name: "t3-code-text", version: "0.0.0" },
459 mcpServers: [],
460 }),
461 });
462
463 const refreshModels = Effect.fn("AntigravityDriver.refreshModels")(
464 function* () {
465 const processScope = yield* Scope.make();
466 yield* Effect.addFinalizer((exit) => Scope.close(processScope, exit));
467 yield* authFlow
468 .withProcess(
469 Scope.close(processScope, Exit.void),
470 Effect.gen(function* () {
471 const runtime = yield* makeDisposableRuntime({});
472 const started = yield* runtime.start();
473 yield* publishCatalog(started, runtime);
474 }),
475 )
476 .pipe(Effect.provideService(Scope.Scope, processScope));
477 },
478 Effect.scoped,
479 Effect.timeoutOrElse({
480 duration: "90 seconds",
481 orElse: () =>
482 Effect.fail(
483 new ProviderDriverError({
484 driver: DRIVER,
485 instanceId,
486 detail: "Antigravity model refresh timed out. Try again or check Google sign-in.",
487 }),
488 ),
489 }),
490 Effect.tapError((cause) =>
491 isAntigravitySignInRequiredError(cause) ? provider.onAuthRequired : Effect.void,
492 ),
493 Effect.mapError((cause) =>
494 cause._tag === "ProviderDriverError"
495 ? cause
496 : new ProviderDriverError({
497 driver: DRIVER,
498 instanceId,
499 detail: isAntigravitySignInRequiredError(cause)
500 ? "Sign in to Antigravity in provider settings before refreshing models."
501 : cause._tag === "ProviderSetupError" &&
502 (cause.operation === "configure" || cause.operation === "start")
503 ? cause.detail
504 : "Could not refresh Antigravity models. The previous model list is unchanged.",
505 cause,
506 }),
507 ),
508 );
509
510 return {
511 instanceId,
512 driverKind: DRIVER,
513 continuationIdentity,
514 displayName,
515 accentColor,
516 enabled,
517 snapshot: provider.snapshot,
518 snapshotForCwd: (cwd) =>
519 !enabled
520 ? provider.snapshot.getSnapshot
521 : discoverAntigravitySkills({ cwd, userHome }).pipe(
522 Effect.provideService(FileSystem.FileSystem, fileSystem),
523 Effect.provideService(Path.Path, path),
524 Effect.flatMap((skills) => provider.snapshotForCwd(cwd, skills)),
525 Effect.mapError(
526 (cause) =>
527 new ProviderDriverError({
528 driver: DRIVER,
529 instanceId,
530 detail: "Could not read Antigravity workspace skills.",
531 cause,
532 }),
533 ),
534 ),
535 orchestrationAdapter,
536 textGeneration,
537 auth: authFlow.controller,
538 refreshModels,
539 } satisfies ProviderInstance;
540 }),
541};