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...@@ -54,6 +54,7 @@ const phase = (auth: AntigravityAuth, value: ProviderAuthState["phase"], session
54const makeHarness = Effect.fn("makeAuthTestHarness")(function* (54const makeHarness = Effect.fn("makeAuthTestHarness")(function* (
55 options: {55 options: {
56 readonly interactive?: boolean;56 readonly interactive?: boolean;
57 readonly authorizationUrls?: ReadonlyArray<string>;
57 readonly supportsLogout?: boolean;58 readonly supportsLogout?: boolean;
58 readonly forwardCallback?: Effect.Effect<void, ProviderSetupError>;59 readonly forwardCallback?: Effect.Effect<void, ProviderSetupError>;
59 } = {},60 } = {},
...@@ -62,12 +63,16 @@ const makeHarness = Effect.fn("makeAuthTestHarness")(function* (...@@ -62,12 +63,16 @@ const makeHarness = Effect.fn("makeAuthTestHarness")(function* (
62 const discovered = yield* Deferred.make<void>();63 const discovered = yield* Deferred.make<void>();
63 const closed = yield* Deferred.make<void>();64 const closed = yield* Deferred.make<void>();
64 const events: string[] = [];65 const events: string[] = [];
66 let receiveAuthorizationUrl:
67 | ((url: string) => Effect.Effect<void, AcpErrors.AcpError>)
68 | undefined;
65 let forwarded = 0;69 let forwarded = 0;
66 let catalog = ["previous-account-model"];70 let catalog = ["previous-account-model"];
67 const auth = yield* makeAntigravityAuth({71 const auth = yield* makeAntigravityAuth({
68 instanceId,72 instanceId,
69 makeRuntime: (input) =>73 makeRuntime: (input) =>
70 Effect.gen(function* () {74 Effect.gen(function* () {
75 receiveAuthorizationUrl = input.onAuthorizationUrl;
71 events.push("process-open");76 events.push("process-open");
72 yield* Effect.addFinalizer(() =>77 yield* Effect.addFinalizer(() =>
73 Effect.gen(function* () {78 Effect.gen(function* () {
...@@ -87,7 +92,9 @@ const makeHarness = Effect.fn("makeAuthTestHarness")(function* (...@@ -87,7 +92,9 @@ const makeHarness = Effect.fn("makeAuthTestHarness")(function* (
87 Effect.gen(function* () {92 Effect.gen(function* () {
88 events.push("authenticate");93 events.push("authenticate");
89 if (options.interactive !== false && input.onAuthorizationUrl) {94 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 }
91 }98 }
92 yield* Deferred.await(authenticated);99 yield* Deferred.await(authenticated);
93 events.push("session-new");100 events.push("session-new");
...@@ -124,10 +131,62 @@ const makeHarness = Effect.fn("makeAuthTestHarness")(function* (...@@ -124,10 +131,62 @@ const makeHarness = Effect.fn("makeAuthTestHarness")(function* (
124 events,131 events,
125 catalog: () => catalog,132 catalog: () => catalog,
126 forwarded: () => forwarded,133 forwarded: () => forwarded,
134 receiveAuthorizationUrl: (url: string) =>
135 Effect.suspend(() =>
136 receiveAuthorizationUrl
137 ? receiveAuthorizationUrl(url)
138 : Effect.die("Authorization URL receiver is not ready."),
139 ),
127 };140 };
128});141});
129142
130it.layer(NodeServices.layer)("AntigravityAuth", (it) => {143it.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
131 it.effect("keeps a remote flow private and waits for native auth and catalog discovery", () =>190 it.effect("keeps a remote flow private and waits for native auth and catalog discovery", () =>
132 Effect.gen(function* () {191 Effect.gen(function* () {
133 const harness = yield* makeHarness();192 const harness = yield* makeHarness();
apps/server/src/provider/AntigravityAuth.ts+6-3
...@@ -20,11 +20,13 @@ import * as SubscriptionRef from "effect/SubscriptionRef";...@@ -20,11 +20,13 @@ import * as SubscriptionRef from "effect/SubscriptionRef";
20import * as AcpErrors from "effect-acp/errors";20import * as AcpErrors from "effect-acp/errors";
2121
22import type { AcpSessionRuntime, AcpSessionRuntimeStartResult } from "./acp/AcpSessionRuntime.ts";22import type { AcpSessionRuntime, AcpSessionRuntimeStartResult } from "./acp/AcpSessionRuntime.ts";
23import { parseAntigravityAuthorizationUrl } from "./antigravityAuthSupport.ts";23import {
24 parseAntigravityAuthorizationUrl,
25 type AntigravityAuthorizationUrl,
26} from "./antigravityAuthSupport.ts";
24import {27import {
25 forwardAntigravityCallback,28 forwardAntigravityCallback,
26 validateAntigravityCallbackUrl,29 validateAntigravityCallbackUrl,
27 type AntigravityPendingCallback,
28} from "./antigravityCallback.ts";30} from "./antigravityCallback.ts";
29import type { ProviderAuthController } from "./Services/ProviderAuthService.ts";31import type { ProviderAuthController } from "./Services/ProviderAuthService.ts";
3032
...@@ -43,7 +45,7 @@ interface AuthFlow {...@@ -43,7 +45,7 @@ interface AuthFlow {
43 readonly ownerSessionId: string;45 readonly ownerSessionId: string;
44 readonly expiresAtMillis: number;46 readonly expiresAtMillis: number;
45 state: ProviderAuthState;47 state: ProviderAuthState;
46 pending: AntigravityPendingCallback | undefined;48 pending: AntigravityAuthorizationUrl | undefined;
47 callbackSent: boolean;49 callbackSent: boolean;
48 fiber: Fiber.Fiber<void> | undefined;50 fiber: Fiber.Fiber<void> | undefined;
49 forwarding: Fiber.Fiber<void, ProviderSetupError> | undefined;51 forwarding: Fiber.Fiber<void, ProviderSetupError> | undefined;
...@@ -239,6 +241,7 @@ export const makeAntigravityAuth = Effect.fn("makeAntigravityAuth")(function* <...@@ -239,6 +241,7 @@ export const makeAntigravityAuth = Effect.fn("makeAntigravityAuth")(function* <
239 Effect.gen(function* () {241 Effect.gen(function* () {
240 if (activeFlow !== flow || operation !== "auth") return;242 if (activeFlow !== flow || operation !== "auth") return;
241 if (flow.pending) {243 if (flow.pending) {
244 if (flow.pending.authorizationUrl === authorization.authorizationUrl) return;
242 return yield* new AcpErrors.AcpTransportError({245 return yield* new AcpErrors.AcpTransportError({
243 detail: "Antigravity started more than one Google sign-in request.",246 detail: "Antigravity started more than one Google sign-in request.",
244 cause: undefined,247 cause: undefined,
apps/server/src/provider/Layers/AntigravityProvider.test.ts+22
...@@ -13,6 +13,7 @@ import * as Layer from "effect/Layer";...@@ -13,6 +13,7 @@ import * as Layer from "effect/Layer";
13import * as Ref from "effect/Ref";13import * as Ref from "effect/Ref";
14import * as Schema from "effect/Schema";14import * as Schema from "effect/Schema";
15import * as Stream from "effect/Stream";15import * as Stream from "effect/Stream";
16import * as TestClock from "effect/testing/TestClock";
16import * as EffectAcpErrors from "effect-acp/errors";17import * as EffectAcpErrors from "effect-acp/errors";
17import type * as EffectAcpSchema from "effect-acp/schema";18import type * as EffectAcpSchema from "effect-acp/schema";
1819
...@@ -411,6 +412,27 @@ it.layer(testLayer)("Antigravity provider snapshots", (it) => {...@@ -411,6 +412,27 @@ it.layer(testLayer)("Antigravity provider snapshots", (it) => {
411 ),412 ),
412 );413 );
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
414 it.effect("distinguishes missing executables from a failed installed executable", () =>436 it.effect("distinguishes missing executables from a failed installed executable", () =>
415 Effect.scoped(437 Effect.scoped(
416 Effect.gen(function* () {438 Effect.gen(function* () {
apps/server/src/provider/Layers/AntigravityProvider.ts+6-2
...@@ -32,6 +32,7 @@ import {...@@ -32,6 +32,7 @@ import {
3232
33const EMPTY_MODEL_CAPABILITIES = createModelCapabilities({ optionDescriptors: [] });33const EMPTY_MODEL_CAPABILITIES = createModelCapabilities({ optionDescriptors: [] });
34const MAX_WORKSPACE_SNAPSHOTS = 32;34const MAX_WORKSPACE_SNAPSHOTS = 32;
35const HEALTH_CHECK_TIMEOUT = "45 seconds";
35const SIGN_IN_MESSAGE = "Sign in with Google to use Antigravity.";36const SIGN_IN_MESSAGE = "Sign in with Google to use Antigravity.";
36const AUTH_UNCHECKED_MESSAGE =37const AUTH_UNCHECKED_MESSAGE =
37 "Antigravity is installed. Google account access is not checked yet.";38 "Antigravity is installed. Google account access is not checked yet.";
...@@ -166,7 +167,10 @@ export const makeAntigravityProvider = Effect.fn("makeAntigravityProvider")(func...@@ -166,7 +167,10 @@ export const makeAntigravityProvider = Effect.fn("makeAntigravityProvider")(func
166 const checkProvider = Effect.fn("checkAntigravityProvider")(function* () {167 const checkProvider = Effect.fn("checkAntigravityProvider")(function* () {
167 if (!settings.enabled) return yield* getSnapshot;168 if (!settings.enabled) return yield* getSnapshot;
168 const before = yield* SubscriptionRef.get(metadata);169 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 );
170 const initialized =174 const initialized =
171 Result.isSuccess(result) && Option.isSome(result.success) ? result.success.value : undefined;175 Result.isSuccess(result) && Option.isSome(result.success) ? result.success.value : undefined;
172 const failure = Result.isFailure(result) ? result.failure : undefined;176 const failure = Result.isFailure(result) ? result.failure : undefined;
...@@ -180,7 +184,7 @@ export const makeAntigravityProvider = Effect.fn("makeAntigravityProvider")(func...@@ -180,7 +184,7 @@ export const makeAntigravityProvider = Effect.fn("makeAntigravityProvider")(func
180 ? "Antigravity is not installed or its executable could not be found."184 ? "Antigravity is not installed or its executable could not be found."
181 : failure185 : failure
182 ? "Antigravity could not complete its local health check."186 ? "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}.`;
184 const supportsTextGeneration =188 const supportsTextGeneration =
185 initialized !== undefined ? yield* options.supportsTextGeneration : false;189 initialized !== undefined ? yield* options.supportsTextGeneration : false;
186 const updatedAt = DateTime.formatIso(yield* DateTime.now);190 const updatedAt = DateTime.formatIso(yield* DateTime.now);
apps/server/src/provider/acp/AcpJsonRpcConnection.test.ts+3-2
...@@ -327,7 +327,7 @@ describe("AcpSessionRuntime", () => {...@@ -327,7 +327,7 @@ describe("AcpSessionRuntime", () => {
327 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),327 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
328 );328 );
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", () =>
331 Effect.gen(function* () {331 Effect.gen(function* () {
332 const lengths: Array<number> = [];332 const lengths: Array<number> = [];
333 for (const logStderr of [false, true]) {333 for (const logStderr of [false, true]) {
...@@ -348,7 +348,8 @@ describe("AcpSessionRuntime", () => {...@@ -348,7 +348,8 @@ describe("AcpSessionRuntime", () => {
348 }).pipe(Effect.scoped);348 }).pipe(Effect.scoped);
349 }349 }
350 expect(lengths.length).toBeGreaterThan(0);350 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);
352 }).pipe(Effect.provide(NodeServices.layer)),353 }).pipe(Effect.provide(NodeServices.layer)),
353 );354 );
354355
apps/server/src/provider/acp/AcpSessionRuntime.ts+2-1
...@@ -66,7 +66,8 @@ const defaultSessionLoadTimeout = Duration.seconds(90);...@@ -66,7 +66,8 @@ const defaultSessionLoadTimeout = Duration.seconds(90);
66const defaultSessionLoadReplayIdleGap = Duration.seconds(2);66const defaultSessionLoadReplayIdleGap = Duration.seconds(2);
67const defaultCancelTimeout = Duration.seconds(15);67const defaultCancelTimeout = Duration.seconds(15);
68const maxStartupMetadataUpdates = 32;68const maxStartupMetadataUpdates = 32;
69const maxStderrChunkLength = 8_192;69// Antigravity can emit an accepted 16 KiB Google authorization URL on stderr.
70const maxStderrChunkLength = 32_768;
7071
71export interface AcpSpawnInput {72export interface AcpSpawnInput {
72 readonly command: string;73 readonly command: string;
apps/server/src/provider/acp/AntigravityAcpSupport.ts+8-2
...@@ -19,7 +19,7 @@ import type * as EffectAcpSchema from "effect-acp/schema";...@@ -19,7 +19,7 @@ import type * as EffectAcpSchema from "effect-acp/schema";
1919
20import { resolveAttachmentPath } from "../../attachmentStore.ts";20import { resolveAttachmentPath } from "../../attachmentStore.ts";
21import {21import {
22 drainAntigravityStderr,22 makeAntigravityStderrHandler,
23 makeAntigravityStdoutTransform,23 makeAntigravityStdoutTransform,
24} from "../antigravityAuthSupport.ts";24} from "../antigravityAuthSupport.ts";
25import * as AcpSessionRuntime from "./AcpSessionRuntime.ts";25import * as AcpSessionRuntime from "./AcpSessionRuntime.ts";
...@@ -73,7 +73,13 @@ export const makeAntigravityAcpRuntime = Effect.fn("makeAntigravityAcpRuntime")(...@@ -73,7 +73,13 @@ export const makeAntigravityAcpRuntime = Effect.fn("makeAntigravityAcpRuntime")(
73 transformStdout: makeAntigravityStdoutTransform(73 transformStdout: makeAntigravityStdoutTransform(
74 input.onAuthorizationUrl ? { onAuthorizationUrl: input.onAuthorizationUrl } : {},74 input.onAuthorizationUrl ? { onAuthorizationUrl: input.onAuthorizationUrl } : {},
75 ),75 ),
76 onStderr: drainAntigravityStderr,76 ...(input.onAuthorizationUrl
77 ? {
78 onStderr: makeAntigravityStderrHandler({
79 onAuthorizationUrl: input.onAuthorizationUrl,
80 }),
81 }
82 : {}),
77 transformSessionUpdate: normalizeAntigravitySessionUpdate,83 transformSessionUpdate: normalizeAntigravitySessionUpdate,
78 }).pipe(84 }).pipe(
79 Layer.provide(85 Layer.provide(
apps/server/src/provider/antigravityAuthSupport.test.ts+56
...@@ -17,6 +17,7 @@ import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawne...@@ -17,6 +17,7 @@ import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawne
17import * as AcpErrors from "effect-acp/errors";17import * as AcpErrors from "effect-acp/errors";
1818
19import {19import {
20 ANTIGRAVITY_AUTH_BROWSER_MARKER,
20 ANTIGRAVITY_AUTH_STDOUT_PREFIX,21 ANTIGRAVITY_AUTH_STDOUT_PREFIX,
21 ANTIGRAVITY_PERSONAL_AUTH,22 ANTIGRAVITY_PERSONAL_AUTH,
22 ANTIGRAVITY_SIGN_IN_REQUIRED_MESSAGE,23 ANTIGRAVITY_SIGN_IN_REQUIRED_MESSAGE,
...@@ -26,6 +27,7 @@ import {...@@ -26,6 +27,7 @@ import {
26 antigravityProfileSettings,27 antigravityProfileSettings,
27 buildAntigravityAcpSpawnInput,28 buildAntigravityAcpSpawnInput,
28 isAntigravitySignInRequiredError,29 isAntigravitySignInRequiredError,
30 makeAntigravityStderrHandler,
29 makeAntigravityStdoutTransform,31 makeAntigravityStdoutTransform,
30 parseAntigravityAuthorizationUrl,32 parseAntigravityAuthorizationUrl,
31 prepareAntigravityProfile,33 prepareAntigravityProfile,
...@@ -388,6 +390,60 @@ describe("Antigravity stdout compatibility", () => {...@@ -388,6 +390,60 @@ describe("Antigravity stdout compatibility", () => {
388 );390 );
389});391});
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
391it.layer(NodeServices.layer)("Antigravity profile preparation", (it) => {447it.layer(NodeServices.layer)("Antigravity profile preparation", (it) => {
392 it.effect("preflights the no-browser helper and creates private directories only", () =>448 it.effect("preflights the no-browser helper and creates private directories only", () =>
393 Effect.gen(function* () {449 Effect.gen(function* () {
apps/server/src/provider/antigravityAuthSupport.ts+32-2
...@@ -24,9 +24,12 @@ export const ANTIGRAVITY_SIGN_IN_REQUIRED_MESSAGE =...@@ -24,9 +24,12 @@ export const ANTIGRAVITY_SIGN_IN_REQUIRED_MESSAGE =
24 "Sign in to Antigravity in Settings before you continue.";24 "Sign in to Antigravity in Settings before you continue.";
2525
26const maxAuthorizationUrlLength = 16_384;26const maxAuthorizationUrlLength = 16_384;
27const maxBrowserHelperLineLength =
28 ANTIGRAVITY_AUTH_BROWSER_MARKER.length + maxAuthorizationUrlLength + 2;
27const maxStdoutLineBytes = 16 * 1024 * 1024;29const maxStdoutLineBytes = 16 * 1024 * 1024;
28const authPrefixBytes = new TextEncoder().encode(ANTIGRAVITY_AUTH_STDOUT_PREFIX);30const authPrefixBytes = new TextEncoder().encode(ANTIGRAVITY_AUTH_STDOUT_PREFIX);
29const decodeUrl = Schema.decodeUnknownEffect(Schema.URLFromString);31const decodeUrl = Schema.decodeUnknownEffect(Schema.URLFromString);
32const decodeBrowserHelperUrl = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.String));
30const ProfileSettingsFile = Schema.Struct({33const ProfileSettingsFile = Schema.Struct({
31 auth: Schema.Struct({ type: Schema.String }),34 auth: Schema.Struct({ type: Schema.String }),
32 gcp: Schema.optional(35 gcp: Schema.optional(
...@@ -455,5 +458,32 @@ export function makeAntigravityStdoutTransform(...@@ -455,5 +458,32 @@ export function makeAntigravityStdoutTransform(
455 });458 });
456}459}
457460
458/** Upstream stderr can contain OAuth URLs, states, and redirect codes. */461/** Receives the URL emitted by T3's no-browser helper on stderr. */
459export const drainAntigravityStderr = (_text: string): Effect.Effect<void> => Effect.void;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}