authorgravatar for 59291417+WellyngtonF@users.noreply.github.comWellyngtonF <59291417+WellyngtonF@users.noreply.github.com> 2026-09-03 20:19:27-03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-03 16:19:27-07:00
loge01c153c18a94a2aa33df7115c6cfee1eb09880f
treeacbbf3db792b4ec10c267d73130e37edd99945b2
parentef4cc6085e24d6309252412c6dab5482ac11a09f
signature Signed by PGP key B5690EEEBB952194

fix(antigravity): forward Google sign-in URLs from browser helper (#9425)


9 files changed, 195 insertions(+), 13 deletions(-)

apps/server/src/provider/AntigravityAuth.test.ts+60-1
......@@ -54,6 +54,7 @@ const phase = (auth: AntigravityAuth, value: ProviderAuthState["phase"], session
5454const makeHarness = Effect.fn("makeAuthTestHarness")(function* (
5555 options: {
5656 readonly interactive?: boolean;
57 readonly authorizationUrls?: ReadonlyArray<string>;
5758 readonly supportsLogout?: boolean;
5859 readonly forwardCallback?: Effect.Effect<void, ProviderSetupError>;
5960 } = {},
......@@ -62,12 +63,16 @@ const makeHarness = Effect.fn("makeAuthTestHarness")(function* (
6263 const discovered = yield* Deferred.make<void>();
6364 const closed = yield* Deferred.make<void>();
6465 const events: string[] = [];
66 let receiveAuthorizationUrl:
67 | ((url: string) => Effect.Effect<void, AcpErrors.AcpError>)
68 | undefined;
6569 let forwarded = 0;
6670 let catalog = ["previous-account-model"];
6771 const auth = yield* makeAntigravityAuth({
6872 instanceId,
6973 makeRuntime: (input) =>
7074 Effect.gen(function* () {
75 receiveAuthorizationUrl = input.onAuthorizationUrl;
7176 events.push("process-open");
7277 yield* Effect.addFinalizer(() =>
7378 Effect.gen(function* () {
......@@ -87,7 +92,9 @@ const makeHarness = Effect.fn("makeAuthTestHarness")(function* (
8792 Effect.gen(function* () {
8893 events.push("authenticate");
8994 if (options.interactive !== false && input.onAuthorizationUrl) {
90 yield* input.onAuthorizationUrl(authorizationUrl);
95 for (const url of options.authorizationUrls ?? [authorizationUrl]) {
96 yield* input.onAuthorizationUrl(url);
97 }
9198 }
9299 yield* Deferred.await(authenticated);
93100 events.push("session-new");
......@@ -124,10 +131,62 @@ const makeHarness = Effect.fn("makeAuthTestHarness")(function* (
124131 events,
125132 catalog: () => catalog,
126133 forwarded: () => forwarded,
134 receiveAuthorizationUrl: (url: string) =>
135 Effect.suspend(() =>
136 receiveAuthorizationUrl
137 ? receiveAuthorizationUrl(url)
138 : Effect.die("Authorization URL receiver is not ready."),
139 ),
127140 };
128141});
129142
130143it.layer(NodeServices.layer)("AntigravityAuth", (it) => {
144 it.effect("accepts the same authorization URL from stderr and stdout", () =>
145 Effect.gen(function* () {
146 const harness = yield* makeHarness({
147 authorizationUrls: [authorizationUrl, authorizationUrl],
148 });
149 yield* harness.auth.controller.start(owner);
150 const waiting = yield* phase(harness.auth, "waiting");
151 assert.equal(waiting.authorizationUrl, authorizationUrl);
152
153 yield* Deferred.succeed(harness.authenticated, undefined);
154 yield* Deferred.succeed(harness.discovered, undefined);
155 yield* phase(harness.auth, "succeeded");
156 }),
157 );
158
159 it.effect("accepts a delayed duplicate after callback completion starts", () =>
160 Effect.gen(function* () {
161 const harness = yield* makeHarness();
162 const state = yield* harness.auth.controller.start(owner);
163 yield* phase(harness.auth, "waiting");
164 yield* harness.auth.controller.complete(owner, {
165 flowId: state.flowId!,
166 callbackUrl,
167 });
168
169 yield* harness.receiveAuthorizationUrl(authorizationUrl);
170
171 yield* Deferred.succeed(harness.authenticated, undefined);
172 yield* Deferred.succeed(harness.discovered, undefined);
173 yield* phase(harness.auth, "succeeded");
174 }),
175 );
176
177 it.effect("rejects a different second authorization URL", () =>
178 Effect.gen(function* () {
179 const harness = yield* makeHarness({
180 authorizationUrls: [authorizationUrl, `${authorizationUrl}&scope=another-request`],
181 });
182 yield* harness.auth.controller.start(owner);
183
184 const failed = yield* phase(harness.auth, "failed");
185 assert.isNull(failed.authorizationUrl);
186 assert.deepEqual(harness.catalog(), ["previous-account-model"]);
187 }),
188 );
189
131190 it.effect("keeps a remote flow private and waits for native auth and catalog discovery", () =>
132191 Effect.gen(function* () {
133192 const harness = yield* makeHarness();
apps/server/src/provider/AntigravityAuth.ts+6-3
......@@ -20,11 +20,13 @@ import * as SubscriptionRef from "effect/SubscriptionRef";
2020import * as AcpErrors from "effect-acp/errors";
2121
2222import type { AcpSessionRuntime, AcpSessionRuntimeStartResult } from "./acp/AcpSessionRuntime.ts";
23import { parseAntigravityAuthorizationUrl } from "./antigravityAuthSupport.ts";
23import {
24 parseAntigravityAuthorizationUrl,
25 type AntigravityAuthorizationUrl,
26} from "./antigravityAuthSupport.ts";
2427import {
2528 forwardAntigravityCallback,
2629 validateAntigravityCallbackUrl,
27 type AntigravityPendingCallback,
2830} from "./antigravityCallback.ts";
2931import type { ProviderAuthController } from "./Services/ProviderAuthService.ts";
3032
......@@ -43,7 +45,7 @@ interface AuthFlow {
4345 readonly ownerSessionId: string;
4446 readonly expiresAtMillis: number;
4547 state: ProviderAuthState;
46 pending: AntigravityPendingCallback | undefined;
48 pending: AntigravityAuthorizationUrl | undefined;
4749 callbackSent: boolean;
4850 fiber: Fiber.Fiber<void> | undefined;
4951 forwarding: Fiber.Fiber<void, ProviderSetupError> | undefined;
......@@ -239,6 +241,7 @@ export const makeAntigravityAuth = Effect.fn("makeAntigravityAuth")(function* <
239241 Effect.gen(function* () {
240242 if (activeFlow !== flow || operation !== "auth") return;
241243 if (flow.pending) {
244 if (flow.pending.authorizationUrl === authorization.authorizationUrl) return;
242245 return yield* new AcpErrors.AcpTransportError({
243246 detail: "Antigravity started more than one Google sign-in request.",
244247 cause: undefined,
apps/server/src/provider/Layers/AntigravityProvider.test.ts+22
......@@ -13,6 +13,7 @@ import * as Layer from "effect/Layer";
1313import * as Ref from "effect/Ref";
1414import * as Schema from "effect/Schema";
1515import * as Stream from "effect/Stream";
16import * as TestClock from "effect/testing/TestClock";
1617import * as EffectAcpErrors from "effect-acp/errors";
1718import type * as EffectAcpSchema from "effect-acp/schema";
1819
......@@ -411,6 +412,27 @@ it.layer(testLayer)("Antigravity provider snapshots", (it) => {
411412 ),
412413 );
413414
415 it.effect("allows a slow packaged runtime health check to finish", () =>
416 Effect.scoped(
417 Effect.gen(function* () {
418 const harness = yield* makeHarness();
419 yield* harness.initialize;
420 yield* Ref.set(harness.probe, Effect.sleep("20 seconds").pipe(Effect.as(initializeResult)));
421
422 const refresh = yield* harness.provider.snapshot.refresh.pipe(Effect.forkChild);
423 yield* Effect.yieldNow;
424 yield* TestClock.adjust("20 seconds");
425 const snapshot = yield* Fiber.join(refresh);
426
427 expect(snapshot).toMatchObject({
428 installed: true,
429 status: "warning",
430 auth: { status: "unknown" },
431 });
432 }),
433 ),
434 );
435
414436 it.effect("distinguishes missing executables from a failed installed executable", () =>
415437 Effect.scoped(
416438 Effect.gen(function* () {
apps/server/src/provider/Layers/AntigravityProvider.ts+6-2
......@@ -32,6 +32,7 @@ import {
3232
3333const EMPTY_MODEL_CAPABILITIES = createModelCapabilities({ optionDescriptors: [] });
3434const MAX_WORKSPACE_SNAPSHOTS = 32;
35const HEALTH_CHECK_TIMEOUT = "45 seconds";
3536const SIGN_IN_MESSAGE = "Sign in with Google to use Antigravity.";
3637const AUTH_UNCHECKED_MESSAGE =
3738 "Antigravity is installed. Google account access is not checked yet.";
......@@ -166,7 +167,10 @@ export const makeAntigravityProvider = Effect.fn("makeAntigravityProvider")(func
166167 const checkProvider = Effect.fn("checkAntigravityProvider")(function* () {
167168 if (!settings.enabled) return yield* getSnapshot;
168169 const before = yield* SubscriptionRef.get(metadata);
169 const result = yield* options.probe.pipe(Effect.timeoutOption("15 seconds"), Effect.result);
170 const result = yield* options.probe.pipe(
171 Effect.timeoutOption(HEALTH_CHECK_TIMEOUT),
172 Effect.result,
173 );
170174 const initialized =
171175 Result.isSuccess(result) && Option.isSome(result.success) ? result.success.value : undefined;
172176 const failure = Result.isFailure(result) ? result.failure : undefined;
......@@ -180,7 +184,7 @@ export const makeAntigravityProvider = Effect.fn("makeAntigravityProvider")(func
180184 ? "Antigravity is not installed or its executable could not be found."
181185 : failure
182186 ? "Antigravity could not complete its local health check."
183 : "Antigravity did not respond to its local health check within 15 seconds.";
187 : `Antigravity did not respond to its local health check within ${HEALTH_CHECK_TIMEOUT}.`;
184188 const supportsTextGeneration =
185189 initialized !== undefined ? yield* options.supportsTextGeneration : false;
186190 const updatedAt = DateTime.formatIso(yield* DateTime.now);
apps/server/src/provider/acp/AcpJsonRpcConnection.test.ts+3-2
......@@ -327,7 +327,7 @@ describe("AcpSessionRuntime", () => {
327327 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
328328 );
329329
330 it.effect("drains large stderr output and bounds optional logging chunks", () =>
330 it.effect("drains large stderr output and keeps auth-sized logging chunks", () =>
331331 Effect.gen(function* () {
332332 const lengths: Array<number> = [];
333333 for (const logStderr of [false, true]) {
......@@ -348,7 +348,8 @@ describe("AcpSessionRuntime", () => {
348348 }).pipe(Effect.scoped);
349349 }
350350 expect(lengths.length).toBeGreaterThan(0);
351 expect(Math.max(...lengths)).toBeLessThanOrEqual(8_192);
351 expect(Math.max(...lengths)).toBeGreaterThanOrEqual(16_384);
352 expect(Math.max(...lengths)).toBeLessThanOrEqual(32_768);
352353 }).pipe(Effect.provide(NodeServices.layer)),
353354 );
354355
apps/server/src/provider/acp/AcpSessionRuntime.ts+2-1
......@@ -66,7 +66,8 @@ const defaultSessionLoadTimeout = Duration.seconds(90);
6666const defaultSessionLoadReplayIdleGap = Duration.seconds(2);
6767const defaultCancelTimeout = Duration.seconds(15);
6868const maxStartupMetadataUpdates = 32;
69const maxStderrChunkLength = 8_192;
69// Antigravity can emit an accepted 16 KiB Google authorization URL on stderr.
70const maxStderrChunkLength = 32_768;
7071
7172export interface AcpSpawnInput {
7273 readonly command: string;
apps/server/src/provider/acp/AntigravityAcpSupport.ts+8-2
......@@ -19,7 +19,7 @@ import type * as EffectAcpSchema from "effect-acp/schema";
1919
2020import { resolveAttachmentPath } from "../../attachmentStore.ts";
2121import {
22 drainAntigravityStderr,
22 makeAntigravityStderrHandler,
2323 makeAntigravityStdoutTransform,
2424} from "../antigravityAuthSupport.ts";
2525import * as AcpSessionRuntime from "./AcpSessionRuntime.ts";
......@@ -73,7 +73,13 @@ export const makeAntigravityAcpRuntime = Effect.fn("makeAntigravityAcpRuntime")(
7373 transformStdout: makeAntigravityStdoutTransform(
7474 input.onAuthorizationUrl ? { onAuthorizationUrl: input.onAuthorizationUrl } : {},
7575 ),
76 onStderr: drainAntigravityStderr,
76 ...(input.onAuthorizationUrl
77 ? {
78 onStderr: makeAntigravityStderrHandler({
79 onAuthorizationUrl: input.onAuthorizationUrl,
80 }),
81 }
82 : {}),
7783 transformSessionUpdate: normalizeAntigravitySessionUpdate,
7884 }).pipe(
7985 Layer.provide(
apps/server/src/provider/antigravityAuthSupport.test.ts+56
......@@ -17,6 +17,7 @@ import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawne
1717import * as AcpErrors from "effect-acp/errors";
1818
1919import {
20 ANTIGRAVITY_AUTH_BROWSER_MARKER,
2021 ANTIGRAVITY_AUTH_STDOUT_PREFIX,
2122 ANTIGRAVITY_PERSONAL_AUTH,
2223 ANTIGRAVITY_SIGN_IN_REQUIRED_MESSAGE,
......@@ -26,6 +27,7 @@ import {
2627 antigravityProfileSettings,
2728 buildAntigravityAcpSpawnInput,
2829 isAntigravitySignInRequiredError,
30 makeAntigravityStderrHandler,
2931 makeAntigravityStdoutTransform,
3032 parseAntigravityAuthorizationUrl,
3133 prepareAntigravityProfile,
......@@ -388,6 +390,60 @@ describe("Antigravity stdout compatibility", () => {
388390 );
389391});
390392
393describe("Antigravity stderr compatibility", () => {
394 it.effect("forwards an accepted browser-helper URL larger than 8 KiB", () =>
395 Effect.gen(function* () {
396 const urls: string[] = [];
397 const longAuthorizationUrl = `${authorizationUrl}&scope=${"a".repeat(9_000)}`;
398 const handleStderr = makeAntigravityStderrHandler({
399 onAuthorizationUrl: (url) => Effect.sync(() => void urls.push(url)),
400 });
401
402 expect(longAuthorizationUrl.length).toBeGreaterThan(8_192);
403 yield* handleStderr(
404 `${ANTIGRAVITY_AUTH_BROWSER_MARKER}${encodeUnknownJson(longAuthorizationUrl)}\n`,
405 );
406
407 expect(urls).toEqual([longAuthorizationUrl]);
408 }),
409 );
410
411 it.effect("forwards a fragmented browser-helper URL without exposing other stderr", () =>
412 Effect.gen(function* () {
413 const urls: string[] = [];
414 const markerLine = `${ANTIGRAVITY_AUTH_BROWSER_MARKER}${encodeUnknownJson(authorizationUrl)}\n`;
415 const handleStderr = makeAntigravityStderrHandler({
416 onAuthorizationUrl: (url) => Effect.sync(() => void urls.push(url)),
417 });
418
419 yield* handleStderr(`native log\n${markerLine.slice(0, 12)}`);
420 yield* handleStderr(markerLine.slice(12, 70));
421 yield* handleStderr(`${markerLine.slice(70)}another native log\n`);
422
423 expect(urls).toEqual([authorizationUrl]);
424 }),
425 );
426
427 it.effect("ignores malformed and similar browser-helper messages", () =>
428 Effect.gen(function* () {
429 const urls: string[] = [];
430 const handleStderr = makeAntigravityStderrHandler({
431 onAuthorizationUrl: (url) => Effect.sync(() => void urls.push(url)),
432 });
433
434 yield* handleStderr(
435 ` ${ANTIGRAVITY_AUTH_BROWSER_MARKER}${encodeUnknownJson(authorizationUrl)}\n`,
436 );
437 yield* handleStderr(`${ANTIGRAVITY_AUTH_BROWSER_MARKER}${authorizationUrl}\n`);
438 yield* handleStderr(
439 `${ANTIGRAVITY_AUTH_BROWSER_MARKER}${encodeUnknownJson("https://example.com")}\n`,
440 );
441
442 expect(urls).toEqual([]);
443 }),
444 );
445});
446
391447it.layer(NodeServices.layer)("Antigravity profile preparation", (it) => {
392448 it.effect("preflights the no-browser helper and creates private directories only", () =>
393449 Effect.gen(function* () {
apps/server/src/provider/antigravityAuthSupport.ts+32-2
......@@ -24,9 +24,12 @@ export const ANTIGRAVITY_SIGN_IN_REQUIRED_MESSAGE =
2424 "Sign in to Antigravity in Settings before you continue.";
2525
2626const maxAuthorizationUrlLength = 16_384;
27const maxBrowserHelperLineLength =
28 ANTIGRAVITY_AUTH_BROWSER_MARKER.length + maxAuthorizationUrlLength + 2;
2729const maxStdoutLineBytes = 16 * 1024 * 1024;
2830const authPrefixBytes = new TextEncoder().encode(ANTIGRAVITY_AUTH_STDOUT_PREFIX);
2931const decodeUrl = Schema.decodeUnknownEffect(Schema.URLFromString);
32const decodeBrowserHelperUrl = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.String));
3033const ProfileSettingsFile = Schema.Struct({
3134 auth: Schema.Struct({ type: Schema.String }),
3235 gcp: Schema.optional(
......@@ -455,5 +458,32 @@ export function makeAntigravityStdoutTransform(
455458 });
456459}
457460
458/** Upstream stderr can contain OAuth URLs, states, and redirect codes. */
459export const drainAntigravityStderr = (_text: string): Effect.Effect<void> => Effect.void;
461/** Receives the URL emitted by T3's no-browser helper on stderr. */
462export function makeAntigravityStderrHandler(input: {
463 readonly onAuthorizationUrl: (
464 authorizationUrl: string,
465 ) => Effect.Effect<void, AcpErrors.AcpError>;
466}) {
467 let pending = "";
468 const handleLine = (line: string) => {
469 const message = line.endsWith("\r") ? line.slice(0, -1) : line;
470 if (
471 message.length > maxBrowserHelperLineLength ||
472 !message.startsWith(ANTIGRAVITY_AUTH_BROWSER_MARKER)
473 ) {
474 return Effect.void;
475 }
476 return decodeBrowserHelperUrl(message.slice(ANTIGRAVITY_AUTH_BROWSER_MARKER.length)).pipe(
477 Effect.flatMap(parseAntigravityAuthorizationUrl),
478 Effect.flatMap((request) => input.onAuthorizationUrl(request.authorizationUrl)),
479 Effect.ignore,
480 );
481 };
482
483 return Effect.fn("antigravityAuthSupport.handleStderr")(function* (text: string) {
484 const lines = `${pending}${text}`.split("\n");
485 pending = lines.pop() ?? "";
486 if (pending.length > maxBrowserHelperLineLength) pending = "";
487 yield* Effect.forEach(lines, handleLine, { discard: true });
488 });
489}