Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion apps/server/scripts/acp-mock-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -563,12 +563,13 @@ const program = Effect.gen(function* () {
}),
);

// Antigravity requires GEMINI_API_KEY for API key authentication.
// Antigravity requires GEMINI_API_KEY for API key authentication. Agent Platform checks credentials on prompt.
yield* agent.handleAuthenticate((request) =>
Effect.gen(function* () {
if (antigravityProfile) {
if (
request.methodId !== "oauth-personal" &&
request.methodId !== "agent-platform" &&
!(request.methodId === "gemini-api-key" && geminiApiKey)
) {
return yield* AcpError.AcpRequestError.invalidParams(
Expand Down Expand Up @@ -1870,6 +1871,13 @@ const program = Effect.gen(function* () {
);

yield* agent.handleUnknownExtRequest((method, params) => {
// Antigravity's sign-out is the raw v1 method, sent through the runtime's generic request.
if (antigravityProfile && method === "logout") {
return Effect.sync(() => {
authenticated = false;
return {};
});
}
if (method === "_test/environment") {
return Effect.succeed({
inherited: process.env.T3_ACP_RUNTIME_AMBIENT === "sentinel",
Expand Down
9 changes: 9 additions & 0 deletions apps/server/src/outboundHttp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ export function installHttp1FetchDispatcher(): void {
);
}

/** A fetch on its own Agent, so neither the global dispatcher nor an env proxy applies. */
export function makeDirectFetch() {
const undici = requireUndici("undici") as typeof Undici;
const dispatcher = new undici.Agent();
const fetch: typeof Undici.fetch = (input, init) => undici.fetch(input, { ...init, dispatcher });
// The same WHATWG fetch, typed by npm undici rather than the copy behind Node's globals.
return { fetch: fetch as typeof globalThis.fetch, close: () => dispatcher.destroy() };
}

/** Avoids uncaught idle-timeout errors on h2 streams (https://github.com/nodejs/undici/issues/5936). */
export function keepOutboundFetchOnHttp1(): void {
if (builtinFetchUsesHttp2(process.versions.undici)) installHttp1FetchDispatcher();
Expand Down
3 changes: 2 additions & 1 deletion apps/server/src/provider/AntigravityAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export interface AntigravityAuthOptions<
readonly instanceId: ProviderInstanceId;
readonly makeRuntime: (input: {
readonly onAuthorizationUrl?: (url: string) => Effect.Effect<void, AcpErrors.AcpError>;
readonly signOut?: boolean;
}) => Effect.Effect<Runtime, AcpErrors.AcpError | ProviderSetupError, Scope.Scope>;
readonly onAuthenticated: (
result: AcpSessionRuntime.AcpSessionRuntimeStartResult,
Expand Down Expand Up @@ -515,7 +516,7 @@ export const makeAntigravityAuth = Effect.fn("makeAntigravityAuth")(function* <
const result = yield* restore(
Effect.gen(function* () {
yield* stopSessions.pipe(Effect.ensuring(stopRemaining));
const runtime = yield* options.makeRuntime({});
const runtime = yield* options.makeRuntime({ signOut: true });
const initialized = yield* runtime.initialize();
if (!initialized.agentCapabilities?.auth?.logout) {
return yield* setupError(
Expand Down
141 changes: 138 additions & 3 deletions apps/server/src/provider/Drivers/AntigravityDriver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import * as Path from "effect/Path";
import * as Redacted from "effect/Redacted";
import * as Result from "effect/Result";
import * as Schema from "effect/Schema";
import * as TestClock from "effect/testing/TestClock";
import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
import { HttpClient } from "effect/http";
import { HttpClient, HttpClientError, HttpClientResponse } from "effect/http";

import * as BackgroundPolicy from "../../background/BackgroundPolicy.ts";
import * as ServerConfig from "../../config.ts";
Expand Down Expand Up @@ -57,7 +59,13 @@ function shellQuote(value: string): string {
}

const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* (
options: { readonly config?: Partial<AntigravitySettings>; readonly enabled?: boolean } = {},
options: {
readonly config?: Partial<AntigravitySettings>;
readonly enabled?: boolean;
readonly environment?: ReadonlyArray<{ readonly name: string; readonly value: string }>;
readonly metadataServer?: boolean;
readonly metadataHost?: string;
} = {},
) {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
Expand Down Expand Up @@ -128,7 +136,13 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* (
selected: AntigravityInstallation.AntigravityExecutable;
failResolution: boolean;
beforeAcquire: Effect.Effect<void>;
} = { selected: first, failResolution: false, beforeAcquire: Effect.void };
metadataServer: boolean;
} = {
selected: first,
failResolution: false,
beforeAcquire: Effect.void,
metadataServer: options.metadataServer ?? false,
};
const acquisitions: Array<{ binaryPath: string | undefined; path: string | undefined }> = [];
const releases: Array<string | null> = [];
const launches: Array<{
Expand Down Expand Up @@ -176,6 +190,25 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* (
return selected;
}),
});
const metadataRequests: Array<string> = [];
const httpClient = HttpClient.make((request) => {
if (!request.url.startsWith(`http://${options.metadataHost ?? "169.254.169.254"}`)) {
return Effect.die("Unexpected quota request");
}
metadataRequests.push(request.url);
return controls.metadataServer
? Effect.succeed(
HttpClientResponse.fromWeb(
request,
new Response(null, { headers: { "Metadata-Flavor": "Google" } }),
),
)
: Effect.fail(
new HttpClientError.HttpClientError({
reason: new HttpClientError.TransportError({ request }),
}),
);
});
const observedSpawner = ChildProcessSpawner.make((command) =>
Effect.gen(function* () {
if (command._tag !== "StandardCommand")
Expand Down Expand Up @@ -221,10 +254,14 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* (
{ name: "GEMINI_HOME", value: "/must-not-be-used" },
{ name: "ANTIGRAVITY_HARNESS_PATH", value: "/must-not-be-used" },
{ name: "BROWSER", value: "must-not-run" },
{ name: "CLOUDSDK_CONFIG", value: "" },
{ name: "GCE_METADATA_HOST", value: "" },
...(options.environment ?? []),
].map((variable) => ({ ...variable, sensitive: false })),
}).pipe(
Effect.provide(layerInstallation),
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, observedSpawner),
Effect.provideService(HttpClient.HttpClient, httpClient),
);
const refresh = instance.refreshModels;
if (!refresh) return yield* Effect.die("Antigravity does not expose model refresh.");
Expand Down Expand Up @@ -259,6 +296,7 @@ const makeHarness = Effect.fn("makeAntigravityDriverHarness")(function* (
acquisitions,
releases,
launches,
metadataRequests,
readRequests,
assertClosed,
};
Expand Down Expand Up @@ -461,6 +499,103 @@ it.layer(layerTest)("AntigravityDriver", (it) => {
}).pipe(Effect.scoped),
);

it.effect.skipIf(windowsHost)(
"reports missing Google Cloud credentials before Agent Platform starts",
() =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const home = yield* fs.makeTempDirectoryScoped({ prefix: "t3-antigravity-home-" });
const h = yield* makeHarness({
config: { authMethod: "agent-platform", gcpProject: "proj", gcpLocation: "us-central1" },
environment: [{ name: "HOME", value: home }],
});
const result = yield* h.refresh().pipe(Effect.result);
expect(Result.isFailure(result) ? result.failure.detail : "refresh succeeded").toContain(
"gcloud auth application-default login",
);
expect((yield* h.instance.snapshot.getSnapshot).auth.status).not.toBe("authenticated");
expect(h.launches).toEqual([]);

const gcloudDirectory = path.join(home, ".config", "gcloud");
yield* fs.makeDirectory(gcloudDirectory, { recursive: true });
yield* fs.writeFileString(
path.join(gcloudDirectory, "application_default_credentials.json"),
"{}",
);
yield* h.refresh();
expect((yield* h.instance.snapshot.getSnapshot).auth.status).toBe("authenticated");
yield* h.assertClosed;
}).pipe(Effect.scoped),
);

it.effect.skipIf(windowsHost)(
"starts Agent Platform with metadata server credentials and no local credentials file",
() =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const home = yield* fs.makeTempDirectoryScoped({ prefix: "t3-antigravity-home-" });
const h = yield* makeHarness({
config: { authMethod: "agent-platform", gcpProject: "proj", gcpLocation: "us-central1" },
environment: [
{ name: "HOME", value: home },
{ name: "GCE_METADATA_HOST", value: "metadata.internal:8080" },
],
metadataHost: "metadata.internal:8080",
metadataServer: false,
});
const error = yield* h.refresh().pipe(Effect.flip);
expect(error.detail).toContain("gcloud auth application-default login");
h.controls.metadataServer = true;
yield* TestClock.adjust("30 seconds");
yield* h.refresh();
yield* h.refresh();
expect((yield* h.instance.snapshot.getSnapshot).auth.status).toBe("authenticated");
expect(h.metadataRequests).toEqual([
"http://metadata.internal:8080",
"http://metadata.internal:8080",
]);
yield* h.assertClosed;
}).pipe(Effect.scoped),
);

it.effect.skipIf(windowsHost)(
"signs out of Agent Platform after its Google Cloud credentials are removed",
() =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const home = yield* fs.makeTempDirectoryScoped({ prefix: "t3-antigravity-home-" });
const credentials = path.join(
home,
".config",
"gcloud",
"application_default_credentials.json",
);
yield* fs.makeDirectory(path.dirname(credentials), { recursive: true });
yield* fs.writeFileString(credentials, "{}");
const h = yield* makeHarness({
config: { authMethod: "agent-platform", gcpProject: "proj", gcpLocation: "us-central1" },
// The mock advertises sign-out only when it lists an auth method.
environment: [
{ name: "HOME", value: home },
{ name: "T3_ACP_AUTH_METHOD_ID", value: "agent-platform" },
],
});
const auth = h.instance.auth;
if (!auth) return yield* Effect.die("Antigravity does not expose sign-in.");
yield* h.refresh();
expect((yield* h.instance.snapshot.getSnapshot).auth.status).toBe("authenticated");

yield* fs.remove(credentials);
const state = yield* auth.logout(Effect.void);
expect(state.message).toBe("Signed out of Google.");
expect((yield* h.instance.snapshot.getSnapshot).auth.status).toBe("unauthenticated");
expect((yield* h.readRequests).map((request) => request.method)).toContain("logout");
yield* h.assertClosed;
}).pipe(Effect.scoped),
);

it.effect.skipIf(windowsHost)("keeps the previous catalog when executable resolution fails", () =>
Effect.gen(function* () {
const h = yield* makeHarness();
Expand Down
44 changes: 36 additions & 8 deletions apps/server/src/provider/Drivers/AntigravityDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,13 @@ import * as AntigravityInstallation from "../AntigravityInstallation.ts";
import * as AntigravityUsage from "./AntigravityUsage.ts";
import {
antigravityAuthConfigIssue,
antigravityAuthIssue,
antigravityAuthLabel,
antigravityAuthUsesBrowser,
buildAntigravityAcpSpawnInput,
isAntigravitySignInRequiredError,
prepareAntigravityProfile,
probeGoogleMetadataServer,
resolveAntigravityInstanceDirectories,
type AntigravityAuthConfig,
} from "../antigravityAuthSupport.ts";
Expand Down Expand Up @@ -147,13 +149,30 @@ export const AntigravityDriver: ProviderDriver<
gcpProject: settings.gcpProject,
gcpLocation: settings.gcpLocation,
};
const authConfigIssue = antigravityAuthConfigIssue(auth);
const processEnvironment = yield* mergeProviderInstanceEnvironment(environment);
const platform = yield* HostProcess.Platform;
const userHome = resolveAntigravityUserHome(
yield* HostProcess.Platform,
platform,
processEnvironment,
yield* HostProcess.HomeDirectory,
);
// Bound probe frequency while allowing recovery from transient metadata failures.
const onGoogleCloud = yield* Effect.cachedWithTTL(
probeGoogleMetadataServer(processEnvironment.GCE_METADATA_HOST || undefined).pipe(
Effect.provideService(HttpClient.HttpClient, httpClient),
),
"30 seconds",
);
// Recheck on every use so credentials created after startup take effect.
const authIssue = antigravityAuthIssue(auth, {
platform,
environment: processEnvironment,
userHome,
onGoogleCloud,
}).pipe(
Effect.provideService(FileSystem.FileSystem, fileSystem),
Effect.provideService(Path.Path, path),
);
const directories = yield* resolveAntigravityInstanceDirectories(
host.paths.stateDir,
instanceId,
Expand Down Expand Up @@ -199,11 +218,14 @@ export const AntigravityDriver: ProviderDriver<

const makeRuntime = Effect.fn("AntigravityDriver.makeRuntime")(function* (
input: Omit<AntigravityAcpRuntimeInput, "spawn" | "childProcessSpawner">,
options: { readonly signOut?: boolean } = {},
): Effect.fn.Return<
AcpSessionRuntime.AcpSessionRuntime["Service"],
AcpErrors.AcpError | ProviderSetupError,
Scope.Scope
> {
// Sign-out needs no Google Cloud credentials, so removing them must not block it.
const authConfigIssue = options.signOut ? antigravityAuthConfigIssue(auth) : yield* authIssue;
if (authConfigIssue !== null) {
return yield* new ProviderSetupError({
instanceId,
Expand Down Expand Up @@ -300,7 +322,9 @@ export const AntigravityDriver: ProviderDriver<
});

const makeDisposableRuntime = Effect.fn("AntigravityDriver.makeDisposableRuntime")(function* (
input: Pick<AntigravityAcpRuntimeInput, "onAuthorizationUrl">,
input: Pick<AntigravityAcpRuntimeInput, "onAuthorizationUrl"> & {
readonly signOut?: boolean;
},
) {
const cwd = yield* fileSystem
.makeTempDirectoryScoped({ prefix: "t3-antigravity-setup-" })
Expand All @@ -325,11 +349,14 @@ export const AntigravityDriver: ProviderDriver<
Effect.provideService(Path.Path, path),
),
);
const runtime = yield* makeRuntime({
cwd,
mcpServers: [],
...(input.onAuthorizationUrl ? { onAuthorizationUrl: input.onAuthorizationUrl } : {}),
});
const runtime = yield* makeRuntime(
{
cwd,
mcpServers: [],
...(input.onAuthorizationUrl ? { onAuthorizationUrl: input.onAuthorizationUrl } : {}),
},
{ signOut: input.signOut === true },
);
return {
...runtime,
start: () =>
Expand Down Expand Up @@ -390,6 +417,7 @@ export const AntigravityDriver: ProviderDriver<
// minute.
const probe = Effect.gen(function* () {
yield* modelCatalog.refreshInBackground;
const authConfigIssue = yield* authIssue;
if (authConfigIssue !== null) {
return yield* new ProviderSetupError({
instanceId,
Expand Down
Loading
Loading