Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions apps/server/src/provider/AntigravityInstallation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import * as Scope from "effect/Scope";
import * as Sink from "effect/Sink";
import * as Stream from "effect/Stream";
import { HttpClient, HttpClientResponse } from "effect/http";
import * as ChildProcess from "effect/process/ChildProcess";
import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";

import * as AntigravityInstallation from "./AntigravityInstallation.ts";
Expand Down Expand Up @@ -451,6 +452,41 @@ it.layer(NodeServices.layer)("Antigravity installation", (it) => {
}),
);

it.effect.skipIf(hostPlatform === "win32")(
"reports IPv6 startup diagnostics and preserves the previous release after SIGABRT",
() =>
Effect.gen(function* () {
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const { installation, stagingReleased } = yield* makeHarness({
previous: true,
useDefaultValidation: true,
}).pipe(
Effect.provideService(
ChildProcessSpawner.ChildProcessSpawner,
ChildProcessSpawner.make((command) =>
spawner.spawn(
command._tag === "StandardCommand" && command.command.endsWith(executableName)
? ChildProcess.make(process.execPath, [
"-e",
"process.stderr.write('enforce_kernel_ipv6_support.cc:70] Check failed: AddressFamilySupported(AF_INET6, &loopback6_ok)', () => process.kill(process.pid, 'SIGABRT'));",
])
: command,
),
),
),
);
yield* installation.start;
expect(yield* terminalState(installation)).toMatchObject({
phase: "failed",
message: expect.stringContaining("IPv6"),
});
expect((yield* installation.state).message).toContain("ipv6.disable=1");
expect((yield* installation.state).message).toContain("wsl --shutdown");
yield* Deferred.await(stagingReleased);
yield* expectPreviousRelease(installation);
}),
);

it.effect("accepts an encoded Content-Length when the body is compressed", () =>
Effect.gen(function* () {
// dl.google.com gzips the archive and reports the encoded size. The decoded
Expand Down
17 changes: 11 additions & 6 deletions apps/server/src/provider/AntigravityInstallation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http";
import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
import * as NodeCrypto from "node:crypto";
import * as NodeFSP from "node:fs/promises";
import * as EffectAcpErrors from "effect-acp/errors";

import * as ServerConfig from "../config.ts";
import { openZipArchive } from "../zipArchive.ts";
Expand Down Expand Up @@ -68,6 +69,7 @@ export class AntigravityInstallationError extends Schema.TaggedError<Antigravity
}
}
const isInstallationError = Schema.is(AntigravityInstallationError);
const isAcpProcessExitedError = Schema.is(EffectAcpErrors.AcpProcessExitedError);

export interface AntigravityExecutable {
readonly executablePath: string;
Expand Down Expand Up @@ -393,12 +395,15 @@ export const makeAntigravityInstallation = Effect.fn("AntigravityInstallation.ma
Effect.provideService(Path.Path, path),
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
Effect.provideService(Crypto.Crypto, crypto),
Effect.mapError(
wrapFailure(
"verify",
"The downloaded Antigravity runtime could not start in this environment.",
),
),
Effect.mapError((cause) => {
if (isInstallationError(cause)) return cause;
const detail = isAcpProcessExitedError(cause)
? /\b(?:AF_INET6|enforce_kernel_ipv6_support)\b/.test(cause.stderr ?? "")
? "Antigravity requires IPv6 socket support. Enable IPv6 in this environment. On WSL, remove ipv6.disable=1 from .wslconfig and run wsl --shutdown."
: `The downloaded Antigravity runtime could not start in this environment.\n${cause.message}`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This copies cause.message (including child stderr) into the installation error's detail, which becomes persisted/user-facing state. Service error messages must be derived from safe structured attributes, not an underlying error message; keep the original error in cause and use a fixed diagnostic here.

Suggested change
: `The downloaded Antigravity runtime could not start in this environment.\n${cause.message}`
: "The downloaded Antigravity runtime could not start in this environment."

Posted via Macroscope — Effect Service Conventions

: "The downloaded Antigravity runtime could not start in this environment.";
return installationError("verify", detail, cause);
}),
);

const install = Effect.fn("AntigravityInstallation.install")(
Expand Down
22 changes: 22 additions & 0 deletions packages/effect-acp/src/_internal/stdio.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,28 @@ describe("ACP child process termination", () => {
}),
);

it.effect.each(["SIGABRT", "SIGTERM"])("retains %s as a process exit", (signal) =>
Effect.gen(function* () {
const cause = PlatformError.systemError({
_tag: "Unknown",
module: "ChildProcess",
method: "exitCode",
cause: new Error(`Process interrupted due to receipt of signal: '${signal}'`),
});
const error = yield* makeTerminationError({
pid: ChildProcessSpawner.ProcessId(43),
exitCode: Effect.fail(cause),
});

assert.instanceOf(error, AcpError.AcpProcessExitedError);
assert.equal(error.signal, signal);
assert.equal(error.code, undefined);
assert.equal(error.pid, 43);
assert.strictEqual(error.cause, cause);
assert.equal(error.message, `ACP process exited after signal ${signal}`);
}),
);

it.effect("retains the process identifier and exact exit-status cause", () =>
Effect.gen(function* () {
const rootCause = new Error("private process diagnostics");
Expand Down
24 changes: 18 additions & 6 deletions packages/effect-acp/src/_internal/stdio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,23 @@ export const makeTerminationError = (
handle: ChildProcessTerminationHandle,
): Effect.Effect<AcpError.AcpError> =>
Effect.match(handle.exitCode, {
onFailure: (cause) =>
new AcpError.AcpTransportError({
operation: "read-process-exit-status",
pid: handle.pid,
cause,
}),
onFailure: (cause) => {
// The Node spawner reports signal exits as a platform error rather than an exit code.
const signal =
cause.reason.module === "ChildProcess" &&
cause.reason.method === "exitCode" &&
cause.reason.cause instanceof Error
? /^Process interrupted due to receipt of signal: '(SIG[A-Z0-9]+)'$/.exec(
cause.reason.cause.message,
)?.[1]
: undefined;
return signal === undefined
? new AcpError.AcpTransportError({
operation: "read-process-exit-status",
pid: handle.pid,
cause,
})
: new AcpError.AcpProcessExitedError({ signal, pid: handle.pid, cause });
},
onSuccess: (code) => new AcpError.AcpProcessExitedError({ code, pid: handle.pid }),
});
7 changes: 6 additions & 1 deletion packages/effect-acp/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,14 +94,19 @@ export class AcpProcessExitedError extends Schema.TaggedError<AcpProcessExitedEr
"AcpProcessExitedError",
{
code: Schema.optional(Schema.Number),
signal: Schema.optionalKey(Schema.String),
pid: Schema.optionalKey(Schema.Int),
stderr: Schema.optionalKey(Schema.String),
cause: Schema.optional(Schema.Defect()),
},
) {
override get message() {
const base =
this.code === undefined ? "ACP process exited" : `ACP process exited with code ${this.code}`;
this.signal !== undefined
? `ACP process exited after signal ${this.signal}`
: this.code === undefined
? "ACP process exited"
: `ACP process exited with code ${this.code}`;
const excerpt = this.stderr?.trim();
return excerpt && excerpt.length > 0 ? `${base}\n${excerpt}` : base;
}
Expand Down
1 change: 1 addition & 0 deletions packages/provider-acp/src/server/AcpSessionRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1446,6 +1446,7 @@ export const make = (
? error
: new EffectAcpErrors.AcpProcessExitedError({
...(error.code !== undefined ? { code: error.code } : {}),
...(error.signal !== undefined ? { signal: error.signal } : {}),
...(error.pid !== undefined ? { pid: error.pid } : {}),
stderr,
...(error.cause !== undefined ? { cause: error.cause } : {}),
Expand Down
3 changes: 3 additions & 0 deletions packages/provider-acp/src/server/stderr.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ describe("AcpStderr", () => {
[
"Invalid project config at /Users/ada/.cursor/cli.json",
"Authorization: Bearer secret-token-value",
"Sign in: https://accounts.google.com/o/oauth2/v2/auth?state=oauth-state-value",
"Visit http://localhost:5733/pair#token=ABCDEF for pairing",
"key=sk-abcdefghijklmnopqrstuv",
].join("\n"),
Expand All @@ -30,6 +31,8 @@ describe("AcpStderr", () => {
expect(excerpt).toContain("[redacted]");
expect(excerpt).not.toContain("secret-token-value");
expect(excerpt).not.toContain("ABCDEF");
expect(excerpt).toContain("[sign-in-url]");
expect(excerpt).not.toContain("oauth-state-value");
expect(excerpt).not.toContain("sk-abcdefghijklmnopqrstuv");
});

Expand Down
2 changes: 2 additions & 0 deletions packages/provider-acp/src/server/stderr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
export const ACP_STDERR_TAIL_MAX_CHARS = 4_096;

const PAIRING_URL_PATTERN = /https?:\/\/[^\s]*\/pair#[^\s]*/gi;
const SIGN_IN_URL_PATTERN = /https?:\/\/accounts\.google\.com[^\s]*/gi;
const BEARER_TOKEN_PATTERN = /\bBearer\s+[A-Za-z0-9._\-+=/]+/gi;
const BASIC_AUTH_PATTERN = /\bAuthorization:\s*Basic\s+\S+/gi;
const API_KEY_HEADER_PATTERN = /\bx-api-key:\s*\S+/gi;
Expand All @@ -28,6 +29,7 @@ export function sanitizeAcpStderrExcerpt(
}
result = result
.replace(PAIRING_URL_PATTERN, "[pairing-url]")
.replace(SIGN_IN_URL_PATTERN, "[sign-in-url]")
.replace(BEARER_TOKEN_PATTERN, "Bearer [redacted]")
.replace(BASIC_AUTH_PATTERN, "Authorization: Basic [redacted]")
.replace(API_KEY_HEADER_PATTERN, "x-api-key: [redacted]")
Expand Down
Loading