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
424 }),424 }),
425 );425 );
426 426
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
427 it.effect("accepts an encoded Content-Length when the body is compressed", () =>502 it.effect("accepts an encoded Content-Length when the body is compressed", () =>
428 Effect.gen(function* () {503 Effect.gen(function* () {
429 // dl.google.com gzips the archive and reports the encoded size. The decoded504 // 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
43const DRIVER = ProviderDriverKind.make("antigravity");43const DRIVER = ProviderDriverKind.make("antigravity");
44const DOWNLOAD_TIMEOUT = "45 minutes";44const DOWNLOAD_TIMEOUT = "45 minutes";
45const VALIDATION_TIMEOUT = "90 seconds";45const VALIDATION_TIMEOUT = "90 seconds";
46const VALIDATION_DIAGNOSTIC_MAX_CHARS = 4_096;
46const FREE_SPACE_MARGIN = 256 * 1024 * 1024;47const FREE_SPACE_MARGIN = 256 * 1024 * 1024;
47const RECORD_MAX_BYTES = 8 * 1024;48const RECORD_MAX_BYTES = 8 * 1024;
48const RELEASE_RECORD = ".install-complete.json";49const RELEASE_RECORD = ".install-complete.json";
@@ -476,7 +477,8 @@ export const makeAntigravityInstallation = Effect.fn("AntigravityInstallation.ma · 427 unmodified lines
476 platform,477 platform,
477 baseEnv: environment,478 baseEnv: environment,
478 });479 });
479 const runtime = yield* makeAntigravityAcpRuntime({480 let diagnosticTail = "";
481 const initialized = yield* makeAntigravityAcpRuntime({
480 spawn: buildAntigravityAcpSpawnInput({482 spawn: buildAntigravityAcpSpawnInput({
481 installation: executable,483 installation: executable,
482 profile,484 profile,
@@ -486,8 +488,27 @@ export const makeAntigravityInstallation = Effect.fn("AntigravityInstallation.ma · 3 unmodified lines
486 cwd: profileDirectory,488 cwd: profileDirectory,
487 childProcessSpawner: spawner,489 childProcessSpawner: spawner,
488 clientInfo: { name: "t3-code", version: "0.0.0" },490 clientInfo: { name: "t3-code", version: "0.0.0" },
489 });491 onDiagnostic: (message) =>
490 const initialized = yield* runtime.initialize();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 );
491 if (512 if (
492 initialized.agentInfo?.name !== "antigravity-acp" ||513 initialized.agentInfo?.name !== "antigravity-acp" ||
493 initialized.agentInfo.version !== expectedVersion ||514 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
37> {37> {
38 readonly childProcessSpawner: ChildProcessSpawner.ChildProcessSpawner["Service"];38 readonly childProcessSpawner: ChildProcessSpawner.ChildProcessSpawner["Service"];
39 readonly onAuthorizationUrl?: (url: string) => Effect.Effect<void, EffectAcpErrors.AcpError>;39 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>;
40 /**42 /**
41 * Advertise `fs.readTextFile` and `fs.writeTextFile`. The agent then routes43 * Advertise `fs.readTextFile` and `fs.writeTextFile`. The agent then routes
42 * workspace reads and writes through T3, which turns each edit into a44 * 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
73 transformStdout: makeAntigravityStdoutTransform(75 transformStdout: makeAntigravityStdoutTransform(
74 input.onAuthorizationUrl ? { onAuthorizationUrl: input.onAuthorizationUrl } : {},76 input.onAuthorizationUrl ? { onAuthorizationUrl: input.onAuthorizationUrl } : {},
75 ),77 ),
76 onStderr: makeAntigravityStderrHandler(78 onStderr: makeAntigravityStderrHandler({
77 input.onAuthorizationUrl ? { onAuthorizationUrl: input.onAuthorizationUrl } : {},79 ...(input.onAuthorizationUrl ? { onAuthorizationUrl: input.onAuthorizationUrl } : {}),
78 ),80 ...(input.onDiagnostic ? { onDiagnostic: input.onDiagnostic } : {}),
81 }),
79 transformSessionUpdate: normalizeAntigravitySessionUpdate,82 transformSessionUpdate: normalizeAntigravitySessionUpdate,
80 }).pipe(83 }).pipe(
81 Layer.provide(84 Layer.provide(
apps/server/src/provider/antigravityAuthSupport.test.ts+20−0
@@ -391,6 +391,26 @@ describe("Antigravity stdout compatibility", () => { · 390 unmodified lines
391});391});
392 392
393describe("Antigravity stderr compatibility", () => {393describe("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
394 it.effect("forwards fragmented native sign-in URLs from runtime 1.1.1", () =>414 it.effect("forwards fragmented native sign-in URLs from runtime 1.1.1", () =>
395 Effect.gen(function* () {415 Effect.gen(function* () {
396 const urls: string[] = [];416 const urls: string[] = [];
apps/server/src/provider/antigravityAuthSupport.ts+13−2
@@ -460,12 +460,13 @@ export function makeAntigravityStdoutTransform( · 459 unmodified lines
460 });460 });
461}461}
462 462
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. */
464export function makeAntigravityStderrHandler(464export function makeAntigravityStderrHandler(
465 input: {465 input: {
466 readonly onAuthorizationUrl?: (466 readonly onAuthorizationUrl?: (
467 authorizationUrl: string,467 authorizationUrl: string,
468 ) => Effect.Effect<void, AcpErrors.AcpError>;468 ) => Effect.Effect<void, AcpErrors.AcpError>;
469 readonly onDiagnostic?: (message: string) => Effect.Effect<void>;
469 } = {},470 } = {},
470) {471) {
471 let pending = "";472 let pending = "";
@@ -479,7 +480,17 @@ export function makeAntigravityStderrHandler( · 7 unmodified lines
479 : message.startsWith(ANTIGRAVITY_AUTH_BROWSER_MARKER)480 : message.startsWith(ANTIGRAVITY_AUTH_BROWSER_MARKER)
480 ? decodeBrowserHelperUrl(message.slice(ANTIGRAVITY_AUTH_BROWSER_MARKER.length))481 ? decodeBrowserHelperUrl(message.slice(ANTIGRAVITY_AUTH_BROWSER_MARKER.length))
481 : undefined;482 : 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 }
483 return url.pipe(494 return url.pipe(
484 Effect.flatMap(parseAntigravityAuthorizationUrl),495 Effect.flatMap(parseAntigravityAuthorizationUrl),
485 Effect.matchEffect({496 Effect.matchEffect({