History

fix(server): preserve Antigravity startup diagnostics

Julius Marminge committed 2 months ago

Commit d54b58b1 parent 087cfb85 files · +138 −8Browse files

Unified · Split

5 files
  1. apps/server/src/provider/AntigravityInstallation.test.ts +75 −0
  2. apps/server/src/provider/AntigravityInstallation.ts +24 −3
  3. apps/server/src/provider/acp/AntigravityAcpSupport.ts +6 −3
  4. apps/server/src/provider/antigravityAuthSupport.test.ts +20 −0
  5. apps/server/src/provider/antigravityAuthSupport.ts +13 −2
apps/server/src/provider/AntigravityInstallation.test.ts+75−0
@@ -424,6 +424,81 @@ it.layer(NodeServices.layer)("Antigravity installation", (it) => { · 423 unmodified lines
424424 }),
425425 );
426426
427+ it.effect.each([
428+ {
429+ name: "IPv6 diagnostic",
430+ stderr: [
431+ "Check failed: AddressFamilySupported(AF_INET6, &loopback6_ok)\n",
432+ "Failed to create an AF_INET6 socket.\n",
433+ ],
434+ expected: "Failed to create an AF_INET6 socket.",
435+ },
436+ {
437+ name: "bounded diagnostic tail",
438+ stderr: [`discarded prefix ${"x".repeat(8_000)}\n`, "last startup diagnostic\n"],
439+ expected: "last startup diagnostic",
440+ },
441+ {
442+ name: "no diagnostic",
443+ stderr: [],
444+ expected: "The downloaded Antigravity runtime could not start in this environment.",
445+ },
446+ ])("preserves startup diagnostics when validation exits before initialize: $name", (testCase) =>
447+ Effect.gen(function* () {
448+ const encoder = new TextEncoder();
449+ const spawner = ChildProcessSpawner.make(
450+ Effect.fn("test.spawnFailingAntigravityValidator")(function* (command) {
451+ if (command._tag !== "StandardCommand") {
452+ return yield* Effect.die("Expected one validation process.");
453+ }
454+ const helper = command.args[0] === "-e";
455+ const exited = yield* Deferred.make<ChildProcessSpawner.ExitCode>();
456+ const terminate = Deferred.succeed(exited, ChildProcessSpawner.ExitCode(1)).pipe(
457+ Effect.asVoid,
458+ );
459+ yield* Effect.addFinalizer(() => terminate);
460+ return ChildProcessSpawner.makeHandle({
461+ pid: ChildProcessSpawner.ProcessId(helper ? 1 : 2),
462+ exitCode: helper
463+ ? Effect.succeed(ChildProcessSpawner.ExitCode(0))
464+ : Deferred.await(exited),
465+ isRunning: Deferred.isDone(exited).pipe(Effect.map((done) => !done)),
466+ kill: () => terminate,
467+ unref: Effect.succeed(Effect.void),
468+ stdin: Sink.drain,
469+ stdout: helper
470+ ? Stream.empty
471+ : Stream.fromEffect(Deferred.await(exited)).pipe(Stream.flatMap(() => Stream.empty)),
472+ stderr: helper
473+ ? Stream.make(
474+ encoder.encode(
475+ `${ANTIGRAVITY_AUTH_BROWSER_MARKER}${encodeJsonString(command.args.at(-1))}\n`,
476+ ),
477+ )
478+ : Stream.fromIterable(testCase.stderr.map((line) => encoder.encode(line))).pipe(
479+ Stream.ensuring(terminate),
480+ ),
481+ all: Stream.empty,
482+ getInputFd: () => Sink.drain,
483+ getOutputFd: () => Stream.empty,
484+ });
485+ }),
486+ );
487+ const { installation, stagingReleased } = yield* makeHarness({
488+ previous: true,
489+ useDefaultValidation: true,
490+ }).pipe(Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner));
491+ yield* installation.start;
492+ const state = yield* terminalState(installation);
493+ expect(state.phase).toBe("failed");
494+ expect(state.message).toContain(testCase.expected);
495+ expect(state.message?.length).toBeLessThan(4_200);
496+ expect(state.message).not.toContain("discarded prefix");
497+ yield* Deferred.await(stagingReleased);
498+ yield* expectPreviousRelease(installation);
499+ }),
500+ );
501+
427502 it.effect("accepts an encoded Content-Length when the body is compressed", () =>
428503 Effect.gen(function* () {
429504 // dl.google.com gzips the archive and reports the encoded size. The decoded
apps/server/src/provider/AntigravityInstallation.ts+24−3
@@ -43,6 +43,7 @@ import { · 42 unmodified lines
4343 const DRIVER = ProviderDriverKind.make("antigravity");
4444 const DOWNLOAD_TIMEOUT = "45 minutes";
4545 const VALIDATION_TIMEOUT = "90 seconds";
46+const VALIDATION_DIAGNOSTIC_MAX_CHARS = 4_096;
4647 const FREE_SPACE_MARGIN = 256 * 1024 * 1024;
4748 const RECORD_MAX_BYTES = 8 * 1024;
4849 const RELEASE_RECORD = ".install-complete.json";
@@ -476,7 +477,8 @@ export const makeAntigravityInstallation = Effect.fn("AntigravityInstallation.ma · 427 unmodified lines
476477 platform,
477478 baseEnv: environment,
478479 });
479− const runtime = yield* makeAntigravityAcpRuntime({
480+ let diagnosticTail = "";
481+ const initialized = yield* makeAntigravityAcpRuntime({
480482 spawn: buildAntigravityAcpSpawnInput({
481483 installation: executable,
482484 profile,
@@ -486,8 +488,27 @@ export const makeAntigravityInstallation = Effect.fn("AntigravityInstallation.ma · 3 unmodified lines
486488 cwd: profileDirectory,
487489 childProcessSpawner: spawner,
488490 clientInfo: { name: "t3-code", version: "0.0.0" },
489− });
490− const initialized = yield* runtime.initialize();
491+ onDiagnostic: (message) =>
492+ Effect.sync(() => {
493+ diagnosticTail = `${diagnosticTail}${message}\n`.slice(
494+ -VALIDATION_DIAGNOSTIC_MAX_CHARS,
495+ );
496+ }),
497+ }).pipe(
498+ Effect.flatMap((runtime) => runtime.initialize()),
499+ Effect.mapError((cause) =>
500+ installationError(
501+ "verify",
502+ [
503+ "The downloaded Antigravity runtime could not start in this environment.",
504+ diagnosticTail.trim(),
505+ ]
506+ .filter(Boolean)
507+ .join("\n\n"),
508+ cause,
509+ ),
510+ ),
511+ );
491512 if (
492513 initialized.agentInfo?.name !== "antigravity-acp" ||
493514 initialized.agentInfo.version !== expectedVersion ||
apps/server/src/provider/acp/AntigravityAcpSupport.ts+6−3
@@ -37,6 +37,8 @@ export interface AntigravityAcpRuntimeInput extends Omit< · 36 unmodified lines
3737 > {
3838 readonly childProcessSpawner: ChildProcessSpawner.ChildProcessSpawner["Service"];
3939 readonly onAuthorizationUrl?: (url: string) => Effect.Effect<void, EffectAcpErrors.AcpError>;
40+ /** Receives native stderr lines with authorization messages excluded. */
41+ readonly onDiagnostic?: (message: string) => Effect.Effect<void>;
4042 /**
4143 * Advertise `fs.readTextFile` and `fs.writeTextFile`. The agent then routes
4244 * workspace reads and writes through T3, which turns each edit into a
@@ -73,9 +75,10 @@ export const makeAntigravityAcpRuntime = Effect.fn("makeAntigravityAcpRuntime")( · 30 unmodified lines
7375 transformStdout: makeAntigravityStdoutTransform(
7476 input.onAuthorizationUrl ? { onAuthorizationUrl: input.onAuthorizationUrl } : {},
7577 ),
76− onStderr: makeAntigravityStderrHandler(
77− input.onAuthorizationUrl ? { onAuthorizationUrl: input.onAuthorizationUrl } : {},
78− ),
78+ onStderr: makeAntigravityStderrHandler({
79+ ...(input.onAuthorizationUrl ? { onAuthorizationUrl: input.onAuthorizationUrl } : {}),
80+ ...(input.onDiagnostic ? { onDiagnostic: input.onDiagnostic } : {}),
81+ }),
7982 transformSessionUpdate: normalizeAntigravitySessionUpdate,
8083 }).pipe(
8184 Layer.provide(
apps/server/src/provider/antigravityAuthSupport.test.ts+20−0
@@ -391,6 +391,26 @@ describe("Antigravity stdout compatibility", () => { · 390 unmodified lines
391391 });
392392
393393 describe("Antigravity stderr compatibility", () => {
394+ it.effect("keeps sign-in URLs out of collected native diagnostics", () =>
395+ Effect.gen(function* () {
396+ const diagnostics: string[] = [];
397+ const handleStderr = makeAntigravityStderrHandler({
398+ onAuthorizationUrl: () => Effect.void,
399+ onDiagnostic: (message) => Effect.sync(() => void diagnostics.push(message)),
400+ });
401+ yield* handleStderr("Failed to create an AF_");
402+ yield* handleStderr("INET6 socket.\r\n");
403+ yield* handleStderr(`${ANTIGRAVITY_AUTH_STDOUT_PREFIX}${authorizationUrl}\n`);
404+ yield* handleStderr(
405+ `${ANTIGRAVITY_AUTH_BROWSER_MARKER}${encodeUnknownJson(authorizationUrl)}\n`,
406+ );
407+ yield* handleStderr(` ${ANTIGRAVITY_AUTH_STDOUT_PREFIX}${authorizationUrl}\n`);
408+ yield* handleStderr(` ${ANTIGRAVITY_AUTH_BROWSER_MARKER}${authorizationUrl}\n`);
409+ yield* handleStderr(`${ANTIGRAVITY_AUTH_BROWSER_MARKER}${authorizationUrl}\n`);
410+ expect(diagnostics).toEqual(["Failed to create an AF_INET6 socket."]);
411+ }),
412+ );
413+
394414 it.effect("forwards fragmented native sign-in URLs from runtime 1.1.1", () =>
395415 Effect.gen(function* () {
396416 const urls: string[] = [];
apps/server/src/provider/antigravityAuthSupport.ts+13−2
@@ -460,12 +460,13 @@ export function makeAntigravityStdoutTransform( · 459 unmodified lines
460460 });
461461 }
462462
463−/** Receives native 1.1.1 sign-in URLs and T3 browser-helper URLs without logging stderr. */
463+/** Routes sign-in URLs separately from optional native diagnostic lines. */
464464 export function makeAntigravityStderrHandler(
465465 input: {
466466 readonly onAuthorizationUrl?: (
467467 authorizationUrl: string,
468468 ) => Effect.Effect<void, AcpErrors.AcpError>;
469+ readonly onDiagnostic?: (message: string) => Effect.Effect<void>;
469470 } = {},
470471 ) {
471472 let pending = "";
@@ -479,7 +480,17 @@ export function makeAntigravityStderrHandler( · 7 unmodified lines
479480 : message.startsWith(ANTIGRAVITY_AUTH_BROWSER_MARKER)
480481 ? decodeBrowserHelperUrl(message.slice(ANTIGRAVITY_AUTH_BROWSER_MARKER.length))
481482 : undefined;
482− if (url === undefined) return Effect.void;
483+ if (url === undefined) {
484+ // Malformed or indented auth messages must not enter installation diagnostics.
485+ const trimmed = message.trimStart();
486+ if (
487+ trimmed.startsWith(ANTIGRAVITY_AUTH_STDOUT_PREFIX) ||
488+ trimmed.startsWith(ANTIGRAVITY_AUTH_BROWSER_MARKER)
489+ ) {
490+ return Effect.void;
491+ }
492+ return input.onDiagnostic?.(message) ?? Effect.void;
493+ }
483494 return url.pipe(
484495 Effect.flatMap(parseAntigravityAuthorizationUrl),
485496 Effect.matchEffect({