From 4da20509cce380846aadbce7b7c16ac16e19ff5d Mon Sep 17 00:00:00 2001 From: macodev00 <273427913+macodev00@users.noreply.github.com> Date: Sun, 20 Sep 2026 09:07:31 +0000 Subject: [PATCH 1/3] fix(server): select Antigravity linux-arm64 on aarch64 hosts Map uname and ACP-registry CPU names (aarch64, x86_64) onto the pinned linux-arm64 archive, prefer the host machine over Node's compile arch, and fail with the existing unsupported message when no ARM build exists. Fixes pingdotgg/t3code#12490 --- .../provider/AntigravityInstallation.test.ts | 65 ++++++++++++++++++- .../src/provider/AntigravityInstallation.ts | 14 ++-- .../src/provider/antigravityRelease.test.ts | 37 +++++++++++ .../server/src/provider/antigravityRelease.ts | 28 +++++++- 4 files changed, 136 insertions(+), 8 deletions(-) create mode 100644 apps/server/src/provider/antigravityRelease.test.ts diff --git a/apps/server/src/provider/AntigravityInstallation.test.ts b/apps/server/src/provider/AntigravityInstallation.test.ts index e2bab831712a..47b16c8a1de4 100644 --- a/apps/server/src/provider/AntigravityInstallation.test.ts +++ b/apps/server/src/provider/AntigravityInstallation.test.ts @@ -130,11 +130,15 @@ interface HarnessOptions { readonly contentLength?: number; readonly contentEncoding?: string; readonly platform?: NodeJS.Platform; + readonly arch?: NodeJS.Architecture; + readonly hostMachine?: string; readonly path?: string; readonly previous?: boolean; readonly fileSystem?: FileSystem.FileSystem; readonly validate?: AntigravityInstallationOptions["validate"]; readonly useDefaultValidation?: boolean; + /** When false, resolve the pinned host archive instead of the fixture zip. */ + readonly pinRelease?: boolean; } const makeHarness = Effect.fn("test.makeAntigravityInstallation")(function* ( @@ -145,9 +149,10 @@ const makeHarness = Effect.fn("test.makeAntigravityInstallation")(function* ( const baseDir = options.baseDir ?? (yield* fs.makeTempDirectoryScoped({ prefix: "t3-agy-test-" })); const platform = options.platform ?? hostPlatform; + const arch = options.arch ?? "x64"; const archive = options.archive ?? completeArchive; const asset = options.asset === undefined ? releaseAsset(archive, platform) : options.asset; - const managedDirectory = path.join(baseDir, "tools", "antigravity-acp", `${platform}-x64`); + const managedDirectory = path.join(baseDir, "tools", "antigravity-acp", `${platform}-${arch}`); if (options.previous) { yield* writeRelease(managedDirectory, { ...releaseAsset(archive, platform), @@ -172,7 +177,8 @@ const makeHarness = Effect.fn("test.makeAntigravityInstallation")(function* ( }); const installation = yield* makeAntigravityInstallation({ baseDir, - releaseAsset: asset, + hostMachine: options.hostMachine ?? arch, + ...(options.pinRelease === false ? {} : { releaseAsset: asset }), ...(options.useDefaultValidation ? {} : { @@ -184,7 +190,7 @@ const makeHarness = Effect.fn("test.makeAntigravityInstallation")(function* ( }).pipe( Effect.provideService(FileSystem.FileSystem, trackedFs), Effect.provideService(HostProcessPlatform, platform), - Effect.provideService(HostProcessArchitecture, "x64"), + Effect.provideService(HostProcessArchitecture, arch), Effect.provideService(HostProcessEnvironment, { PATH: options.path ?? "" }), Effect.provideService( HttpClient.HttpClient, @@ -931,4 +937,57 @@ it.layer(NodeServices.layer)("Antigravity installation", (it) => { expect(requests).toEqual([]); }), ); + + it.effect("pins the official linux-arm64 archive on arm64 hosts without downloading", () => + Effect.gen(function* () { + const { installation, requests, path, baseDir } = yield* makeHarness({ + pinRelease: false, + platform: "linux", + arch: "arm64", + }); + expect(yield* installation.state).toMatchObject({ + phase: "idle", + totalBytes: 656_572_786, + version: "agy_acp_server_1.1.1", + }); + expect(installation.managedDirectory).toBe( + path.join(baseDir, "tools", "antigravity-acp", "linux-arm64"), + ); + expect(requests).toEqual([]); + }), + ); + + it.effect("prefers the aarch64 host machine over an x64 Node compile arch", () => + Effect.gen(function* () { + const { installation, path, baseDir } = yield* makeHarness({ + pinRelease: false, + platform: "linux", + arch: "x64", + hostMachine: "aarch64", + }); + expect(yield* installation.state).toMatchObject({ + totalBytes: 656_572_786, + version: "agy_acp_server_1.1.1", + }); + expect(installation.managedDirectory).toBe( + path.join(baseDir, "tools", "antigravity-acp", "linux-arm64"), + ); + }), + ); + + it.effect("refuses linux hosts with no published CPU archive without downloading", () => + Effect.gen(function* () { + const { installation, requests } = yield* makeHarness({ + pinRelease: false, + platform: "linux", + arch: "ia32", + }); + expect(yield* installation.start.pipe(Effect.flip)).toMatchObject({ + operation: "start", + detail: expect.stringMatching(/does not publish an Antigravity runtime for linux-ia32/u), + }); + expect(yield* installation.state).toMatchObject({ phase: "idle", operationId: null }); + expect(requests).toEqual([]); + }), + ); }); diff --git a/apps/server/src/provider/AntigravityInstallation.ts b/apps/server/src/provider/AntigravityInstallation.ts index 24eb4e3d6df8..bc875d423d0b 100644 --- a/apps/server/src/provider/AntigravityInstallation.ts +++ b/apps/server/src/provider/AntigravityInstallation.ts @@ -27,6 +27,7 @@ import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstab import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner"; import * as NodeCrypto from "node:crypto"; import * as NodeFSP from "node:fs/promises"; +import * as NodeOS from "node:os"; import type * as NodeStream from "node:stream"; import * as Yauzl from "yauzl"; @@ -37,6 +38,7 @@ import { prepareAntigravityProfile, } from "./antigravityAuthSupport.ts"; import { + antigravityReleaseArch, resolveAntigravityReleaseAsset, type AntigravityReleaseAsset, } from "./antigravityRelease.ts"; @@ -120,6 +122,8 @@ export class AntigravityInstallation extends Context.Service< export interface AntigravityInstallationOptions { readonly baseDir: string; readonly releaseAsset?: AntigravityReleaseAsset | null; + /** uname -m when known. Defaults to the host machine so aarch64 wins over an x64 Node. */ + readonly hostMachine?: string; readonly validate?: ( executable: AntigravityExecutable, expectedVersion: string, @@ -275,16 +279,18 @@ export const makeAntigravityInstallation = Effect.fn("AntigravityInstallation.ma const platform = yield* HostProcessPlatform; const arch = yield* HostProcessArchitecture; const environment = yield* HostProcessEnvironment; + const hostMachine = options.hostMachine ?? NodeOS.machine(); + const releaseArch = antigravityReleaseArch(hostMachine) ?? antigravityReleaseArch(arch) ?? arch; const releaseAsset = options.releaseAsset === undefined - ? resolveAntigravityReleaseAsset(platform, arch) + ? resolveAntigravityReleaseAsset(platform, arch, hostMachine) : options.releaseAsset; const names = executableNames(platform); const managedDirectory = path.join( options.baseDir, "tools", "antigravity-acp", - `${platform}-${arch}`, + `${platform}-${releaseArch}`, ); const versionsDirectory = path.join(managedDirectory, "versions"); const activePath = path.join(managedDirectory, "active.json"); @@ -432,7 +438,7 @@ export const makeAntigravityInstallation = Effect.fn("AntigravityInstallation.ma "resolve", releaseAsset ? "Antigravity is not installed. Install it in this environment or set a custom executable path." - : `Google does not publish an Antigravity runtime for ${platform}-${arch}. Use a supported environment or a custom executable.`, + : `Google does not publish an Antigravity runtime for ${platform}-${releaseArch}. Use a supported environment or a custom executable.`, ); }, Effect.mapError( @@ -798,7 +804,7 @@ export const makeAntigravityInstallation = Effect.fn("AntigravityInstallation.ma if (!releaseAsset) { return yield* installationError( "start", - `Google does not publish an Antigravity runtime for ${platform}-${arch}. Use a supported remote environment or a custom executable.`, + `Google does not publish an Antigravity runtime for ${platform}-${releaseArch}. Use a supported remote environment or a custom executable.`, ); } const operationId = yield* crypto.randomUUIDv4; diff --git a/apps/server/src/provider/antigravityRelease.test.ts b/apps/server/src/provider/antigravityRelease.test.ts new file mode 100644 index 000000000000..9b281df9b2b3 --- /dev/null +++ b/apps/server/src/provider/antigravityRelease.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from "@effect/vitest"; + +import { antigravityReleaseArch, resolveAntigravityReleaseAsset } from "./antigravityRelease.ts"; + +describe("antigravityRelease", () => { + it("maps Node, uname, and ACP-registry CPU names onto the pinned asset keys", () => { + expect(antigravityReleaseArch("arm64")).toBe("arm64"); + expect(antigravityReleaseArch("aarch64")).toBe("arm64"); + expect(antigravityReleaseArch("AARCH64")).toBe("arm64"); + expect(antigravityReleaseArch("x64")).toBe("x64"); + expect(antigravityReleaseArch("x86_64")).toBe("x64"); + expect(antigravityReleaseArch("amd64")).toBe("x64"); + expect(antigravityReleaseArch("ia32")).toBeNull(); + expect(antigravityReleaseArch("arm")).toBeNull(); + }); + + it("selects the linux-arm64 runtime for Node arm64 and uname aarch64", () => { + const arm64 = resolveAntigravityReleaseAsset("linux", "arm64"); + const aarch64 = resolveAntigravityReleaseAsset("linux", "aarch64"); + expect(arm64?.url).toContain("linux-arm64.zip"); + expect(aarch64).toEqual(arm64); + expect(aarch64?.archiveBytes).toBe(656_572_786); + }); + + it("prefers the host machine when Node was compiled for a different CPU", () => { + const fromX64NodeOnArm = resolveAntigravityReleaseAsset("linux", "x64", "aarch64"); + expect(fromX64NodeOnArm).toEqual(resolveAntigravityReleaseAsset("linux", "arm64")); + const fromArmNodeOnX64 = resolveAntigravityReleaseAsset("linux", "arm64", "x86_64"); + expect(fromArmNodeOnX64).toEqual(resolveAntigravityReleaseAsset("linux", "x64")); + }); + + it("does not invent an archive for unpublished CPUs", () => { + expect(resolveAntigravityReleaseAsset("linux", "ia32")).toBeNull(); + expect(resolveAntigravityReleaseAsset("linux", "arm")).toBeNull(); + expect(resolveAntigravityReleaseAsset("freebsd", "arm64")).toBeNull(); + }); +}); diff --git a/apps/server/src/provider/antigravityRelease.ts b/apps/server/src/provider/antigravityRelease.ts index 90ba309c10c6..8d64243b6513 100644 --- a/apps/server/src/provider/antigravityRelease.ts +++ b/apps/server/src/provider/antigravityRelease.ts @@ -15,6 +15,23 @@ export interface AntigravityReleaseAsset { }; } +/** Asset-map CPU names. Node uses arm64/x64; uname and the ACP registry use aarch64/x86_64. */ +export type AntigravityReleaseArch = "arm64" | "x64"; + +const RELEASE_ARCH_ALIASES: Record = { + arm64: "arm64", + aarch64: "arm64", + arm64e: "arm64", + x64: "x64", + x86_64: "x64", + amd64: "x64", +}; + +/** Maps Node, uname, and ACP-registry CPU names onto the pinned `${platform}-${arch}` keys. */ +export function antigravityReleaseArch(arch: string): AntigravityReleaseArch | null { + return RELEASE_ARCH_ALIASES[arch.trim().toLowerCase()] ?? null; +} + // URLs come from the official registry. Hashes and sizes were checked on 2026-09-03. // https://github.com/agentclientprotocol/registry/blob/81bf71b55e15f630c4fb8a86d20d3088071d2071/antigravity-acp/agent.json const releaseAssets = new Map([ @@ -78,6 +95,15 @@ const releaseAssets = new Map([ export function resolveAntigravityReleaseAsset( platform: NodeJS.Platform, arch: string, + hostMachine: string = arch, ): AntigravityReleaseAsset | null { - return releaseAssets.get(`${platform}-${arch}`) ?? null; + // Prefer the host machine (uname -m / registry aarch64) when it has a pin, + // then the Node compile arch, so linux aarch64 never falls through to x64. + for (const candidate of [hostMachine, arch]) { + const normalized = antigravityReleaseArch(candidate); + if (!normalized) continue; + const asset = releaseAssets.get(`${platform}-${normalized}`); + if (asset) return asset; + } + return null; } From 83f87065d1f72ea7fc7bf86d21750fbb127a5ba3 Mon Sep 17 00:00:00 2001 From: macodev00 <273427913+macodev00@users.noreply.github.com> Date: Sun, 20 Sep 2026 09:10:53 +0000 Subject: [PATCH 2/3] fix(server): resolve Antigravity custom paths on linux aarch64 Expand ~/ in the custom binary path and, when that path is a directory, look up agy_acp_server.par plus localharness_external inside it so a manual CDN extract passes the health check. Keep host-machine aarch64 mapping onto the pinned linux-arm64 archive. Fixes #12490 Fixes #12477 --- .../provider/AntigravityInstallation.test.ts | 22 +++++++++++++++++++ .../src/provider/AntigravityInstallation.ts | 19 +++++++++++----- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/apps/server/src/provider/AntigravityInstallation.test.ts b/apps/server/src/provider/AntigravityInstallation.test.ts index 47b16c8a1de4..504d9e77e069 100644 --- a/apps/server/src/provider/AntigravityInstallation.test.ts +++ b/apps/server/src/provider/AntigravityInstallation.test.ts @@ -22,6 +22,8 @@ import * as Stream from "effect/Stream"; import { HttpClient, HttpClientResponse } from "effect/unstable/http"; import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner"; import * as NodeCrypto from "node:crypto"; +import * as NodeOS from "node:os"; +import * as NodePath from "node:path"; import { makeAntigravityInstallation, @@ -746,6 +748,18 @@ it.layer(NodeServices.layer)("Antigravity installation", (it) => { source: "override", managedVersionDirectory: null, }); + expect(yield* installation.resolve(externalDirectory)).toMatchObject({ + executablePath: externalExecutable, + source: "override", + managedVersionDirectory: null, + }); + const relativeFromHome = NodePath.relative(NodeOS.homedir(), externalDirectory); + expect( + yield* installation.resolve(`~/${relativeFromHome.split(NodePath.sep).join("/")}`), + ).toMatchObject({ + executablePath: externalExecutable, + source: "override", + }); expect(yield* installation.resolve(executableName)).toMatchObject({ source: "override", }); @@ -753,11 +767,19 @@ it.layer(NodeServices.layer)("Antigravity installation", (it) => { expect(yield* installation.resolve(externalExecutable).pipe(Effect.flip)).toMatchObject({ operation: "resolve", }); + expect(yield* installation.resolve(externalDirectory).pipe(Effect.flip)).toMatchObject({ + operation: "resolve", + }); expect( yield* installation.resolve(path.join(baseDir, "missing")).pipe(Effect.flip), ).toMatchObject({ operation: "resolve", }); + const emptyDirectory = path.join(baseDir, "empty"); + yield* fs.makeDirectory(emptyDirectory); + expect(yield* installation.resolve(emptyDirectory).pipe(Effect.flip)).toMatchObject({ + operation: "resolve", + }); yield* expectPreviousRelease(installation); yield* fs.writeFileString(externalHarness, "external harness", { mode: 0o755 }); yield* installation.remove(); diff --git a/apps/server/src/provider/AntigravityInstallation.ts b/apps/server/src/provider/AntigravityInstallation.ts index bc875d423d0b..30b756c3efb6 100644 --- a/apps/server/src/provider/AntigravityInstallation.ts +++ b/apps/server/src/provider/AntigravityInstallation.ts @@ -32,6 +32,7 @@ import type * as NodeStream from "node:stream"; import * as Yauzl from "yauzl"; import { ServerConfig } from "../config.ts"; +import { expandHomePathWith } from "../pathExpansion.ts"; import { makeAntigravityAcpRuntime } from "./acp/AntigravityAcpSupport.ts"; import { buildAntigravityAcpSpawnInput, @@ -372,8 +373,15 @@ export const makeAntigravityInstallation = Effect.fn("AntigravityInstallation.ma candidate: string, source: "override" | "path", ) { - if (!(yield* executableFile(candidate))) return null; - const executablePath = yield* fs.realPath(candidate); + // Manual CDN extracts are often a directory. Health checks resolve the + // ACP file inside it instead of treating the folder as missing. + const info = yield* fs.stat(candidate).pipe(Effect.option); + const executableCandidate = + Option.isSome(info) && info.value.type === "Directory" + ? path.join(candidate, names.executable) + : candidate; + if (!(yield* executableFile(executableCandidate))) return null; + const executablePath = yield* fs.realPath(executableCandidate); const directory = path.dirname(executablePath); const harnessPath = path.join(directory, names.harness); if (!(yield* executableFile(harnessPath))) return null; @@ -413,10 +421,11 @@ export const makeAntigravityInstallation = Effect.fn("AntigravityInstallation.ma function* (binaryPath?: string, processEnvironment?: NodeJS.ProcessEnv) { const override = binaryPath?.trim(); if (override) { + const expanded = expandHomePathWith(override, path); const candidates = - path.isAbsolute(override) || override.includes("/") || override.includes("\\") - ? [path.resolve(override)] - : pathCandidates(override, processEnvironment); + path.isAbsolute(expanded) || expanded.includes("/") || expanded.includes("\\") + ? [path.resolve(expanded)] + : pathCandidates(expanded, processEnvironment); for (const candidate of candidates) { const selected = yield* fromExternal(candidate, "override"); if (selected) return selected; From 9904313a3dd8c494076ce7e9bd7ae69c83043689 Mon Sep 17 00:00:00 2001 From: macodev00 <273427913+macodev00@users.noreply.github.com> Date: Sun, 20 Sep 2026 09:58:36 +0000 Subject: [PATCH 3/3] docs(server): add JSDoc on Antigravity release and path resolve CodeRabbit docstring coverage on the linux-arm64 change was 50% because resolveAntigravityReleaseAsset had no TSDoc. Add one-line docs on the functions that diff already touched. Co-authored-by: maco --- apps/server/src/provider/AntigravityInstallation.test.ts | 1 + apps/server/src/provider/AntigravityInstallation.ts | 3 +++ apps/server/src/provider/antigravityRelease.ts | 1 + 3 files changed, 5 insertions(+) diff --git a/apps/server/src/provider/AntigravityInstallation.test.ts b/apps/server/src/provider/AntigravityInstallation.test.ts index 504d9e77e069..4b15ec4a23ef 100644 --- a/apps/server/src/provider/AntigravityInstallation.test.ts +++ b/apps/server/src/provider/AntigravityInstallation.test.ts @@ -143,6 +143,7 @@ interface HarnessOptions { readonly pinRelease?: boolean; } +/** Build an isolated Antigravity installer fixture, optionally pinning the host archive. */ const makeHarness = Effect.fn("test.makeAntigravityInstallation")(function* ( options: HarnessOptions = {}, ) { diff --git a/apps/server/src/provider/AntigravityInstallation.ts b/apps/server/src/provider/AntigravityInstallation.ts index 30b756c3efb6..bd9ffdef6963 100644 --- a/apps/server/src/provider/AntigravityInstallation.ts +++ b/apps/server/src/provider/AntigravityInstallation.ts @@ -268,6 +268,7 @@ const openArchive = Effect.fn("AntigravityInstallation.openArchive")(function* ( return { entryCount: opened.zip.entryCount, next, streamEntry }; }); +/** Own the environment Antigravity runtime, using the host machine arch for managed installs. */ export const makeAntigravityInstallation = Effect.fn("AntigravityInstallation.make")(function* ( options: AntigravityInstallationOptions, ) { @@ -369,6 +370,7 @@ export const makeAntigravityInstallation = Effect.fn("AntigravityInstallation.ma } satisfies AntigravityExecutable; }); + /** Resolve a custom path, including a directory that contains the ACP executable. */ const fromExternal = Effect.fn("AntigravityInstallation.fromExternal")(function* ( candidate: string, source: "override" | "path", @@ -415,6 +417,7 @@ export const makeAntigravityInstallation = Effect.fn("AntigravityInstallation.ma .map((directory) => path.resolve(directory, binary)); }; + /** Find a managed, PATH, or custom Antigravity executable, expanding `~` in overrides. */ const resolve: AntigravityInstallationService["resolve"] = Effect.fn( "AntigravityInstallation.resolve", )( diff --git a/apps/server/src/provider/antigravityRelease.ts b/apps/server/src/provider/antigravityRelease.ts index 8d64243b6513..87f8047de098 100644 --- a/apps/server/src/provider/antigravityRelease.ts +++ b/apps/server/src/provider/antigravityRelease.ts @@ -92,6 +92,7 @@ const releaseAssets = new Map([ ], ]); +/** Pick the pinned Antigravity archive, preferring the host machine CPU over Node's compile arch. */ export function resolveAntigravityReleaseAsset( platform: NodeJS.Platform, arch: string,