Skip to content

Commit c0bbee0

Browse files
committed
fix(antigravity): forward Google sign-in URLs from browser helper
- Read no-browser helper URLs from stderr without exposing other output - Allow packaged runtime health checks up to 45 seconds
1 parent fff33f9 commit c0bbee0

5 files changed

Lines changed: 107 additions & 6 deletions

File tree

‎apps/server/src/provider/Layers/AntigravityProvider.test.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import * as Layer from "effect/Layer";
1313
import * as Ref from "effect/Ref";
1414
import * as Schema from "effect/Schema";
1515
import * as Stream from "effect/Stream";
16+
import * as TestClock from "effect/testing/TestClock";
1617
import * as EffectAcpErrors from "effect-acp/errors";
1718
import 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‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232

3333
const EMPTY_MODEL_CAPABILITIES = createModelCapabilities({ optionDescriptors: [] });
3434
const MAX_WORKSPACE_SNAPSHOTS = 32;
35+
const HEALTH_CHECK_TIMEOUT = "45 seconds";
3536
const SIGN_IN_MESSAGE = "Sign in with Google to use Antigravity.";
3637
const 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/AntigravityAcpSupport.ts‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import type * as EffectAcpSchema from "effect-acp/schema";
1919

2020
import { resolveAttachmentPath } from "../../attachmentStore.ts";
2121
import {
22-
drainAntigravityStderr,
22+
makeAntigravityStderrHandler,
2323
makeAntigravityStdoutTransform,
2424
} from "../antigravityAuthSupport.ts";
2525
import * 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‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawne
1717
import * as AcpErrors from "effect-acp/errors";
1818

1919
import {
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,43 @@ describe("Antigravity stdout compatibility", () => {
388390
);
389391
});
390392

393+
describe("Antigravity stderr compatibility", () => {
394+
it.effect("forwards a fragmented browser-helper URL without exposing other stderr", () =>
395+
Effect.gen(function* () {
396+
const urls: string[] = [];
397+
const markerLine = `${ANTIGRAVITY_AUTH_BROWSER_MARKER}${encodeUnknownJson(authorizationUrl)}\n`;
398+
const handleStderr = makeAntigravityStderrHandler({
399+
onAuthorizationUrl: (url) => Effect.sync(() => void urls.push(url)),
400+
});
401+
402+
yield* handleStderr(`native log\n${markerLine.slice(0, 12)}`);
403+
yield* handleStderr(markerLine.slice(12, 70));
404+
yield* handleStderr(`${markerLine.slice(70)}another native log\n`);
405+
406+
expect(urls).toEqual([authorizationUrl]);
407+
}),
408+
);
409+
410+
it.effect("ignores malformed and similar browser-helper messages", () =>
411+
Effect.gen(function* () {
412+
const urls: string[] = [];
413+
const handleStderr = makeAntigravityStderrHandler({
414+
onAuthorizationUrl: (url) => Effect.sync(() => void urls.push(url)),
415+
});
416+
417+
yield* handleStderr(
418+
` ${ANTIGRAVITY_AUTH_BROWSER_MARKER}${encodeUnknownJson(authorizationUrl)}\n`,
419+
);
420+
yield* handleStderr(`${ANTIGRAVITY_AUTH_BROWSER_MARKER}${authorizationUrl}\n`);
421+
yield* handleStderr(
422+
`${ANTIGRAVITY_AUTH_BROWSER_MARKER}${encodeUnknownJson("https://example.com")}\n`,
423+
);
424+
425+
expect(urls).toEqual([]);
426+
}),
427+
);
428+
});
429+
391430
it.layer(NodeServices.layer)("Antigravity profile preparation", (it) => {
392431
it.effect("preflights the no-browser helper and creates private directories only", () =>
393432
Effect.gen(function* () {

‎apps/server/src/provider/antigravityAuthSupport.ts‎

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,12 @@ export const ANTIGRAVITY_SIGN_IN_REQUIRED_MESSAGE =
2424
"Sign in to Antigravity in Settings before you continue.";
2525

2626
const maxAuthorizationUrlLength = 16_384;
27+
const maxBrowserHelperLineLength =
28+
ANTIGRAVITY_AUTH_BROWSER_MARKER.length + maxAuthorizationUrlLength + 2;
2729
const maxStdoutLineBytes = 16 * 1024 * 1024;
2830
const authPrefixBytes = new TextEncoder().encode(ANTIGRAVITY_AUTH_STDOUT_PREFIX);
2931
const decodeUrl = Schema.decodeUnknownEffect(Schema.URLFromString);
32+
const decodeBrowserHelperUrl = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.String));
3033
const 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. */
459-
export const drainAntigravityStderr = (_text: string): Effect.Effect<void> => Effect.void;
461+
/** Receives the URL emitted by T3's no-browser helper on stderr. */
462+
export 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+
}

0 commit comments

Comments
 (0)