From 8eb7405e4946afa5c097840d94bbb151f22d8f90 Mon Sep 17 00:00:00 2001 From: Max Weinbach Date: Mon, 12 Oct 2026 01:22:17 -0400 Subject: [PATCH 1/3] fix(providers): fall back to windows-x86_64 ACP binaries on Windows on ARM Windows 11 on ARM runs x64 binaries under Prism emulation, so agents that only publish windows-x86_64 no longer fail with unsupported_distribution. Archive extraction now uses System32\tar.exe on Windows. Co-Authored-By: Claude Opus 5.5 --- .../src/server/AcpRegistrySupport.test.ts | 41 +++++++++++++ .../src/server/AcpRegistrySupport.ts | 58 +++++++++++++------ 2 files changed, 82 insertions(+), 17 deletions(-) diff --git a/packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts b/packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts index f95286d004af..53628292c193 100644 --- a/packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts +++ b/packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts @@ -209,6 +209,47 @@ describe("AcpRegistrySupport", () => { platformTarget: "darwin-aarch64", }), ).toBeUndefined(); + expect( + AcpRegistrySupport.resolveAcpRegistryDistribution({ + agent: makeAgent({ + binary: { + "windows-x86_64": { + archive: archiveUrl, + cmd: "./bin/example-agent-x64.exe", + args: ["acp"], + }, + }, + }), + preference: "auto", + platformTarget: "windows-aarch64", + }), + ).toMatchObject({ + kind: "binary", + binaryTarget: { cmd: "./bin/example-agent-x64.exe" }, + }); + expect( + AcpRegistrySupport.resolveAcpRegistryDistribution({ + agent: makeAgent({ + binary: { + "windows-aarch64": { + archive: archiveUrl, + cmd: "./bin/example-agent-arm64.exe", + args: ["acp"], + }, + "windows-x86_64": { + archive: archiveUrl, + cmd: "./bin/example-agent-x64.exe", + args: ["acp"], + }, + }, + }), + preference: "auto", + platformTarget: "windows-aarch64", + }), + ).toMatchObject({ + kind: "binary", + binaryTarget: { cmd: "./bin/example-agent-arm64.exe" }, + }); }); it.effect("resolves a local command on the selected environment without the registry", () => { diff --git a/packages/provider-acp-registry/src/server/AcpRegistrySupport.ts b/packages/provider-acp-registry/src/server/AcpRegistrySupport.ts index aeb792b08ca8..897408c603e4 100644 --- a/packages/provider-acp-registry/src/server/AcpRegistrySupport.ts +++ b/packages/provider-acp-registry/src/server/AcpRegistrySupport.ts @@ -12,6 +12,7 @@ import { } from "@t3tools/contracts"; import type { AcpRegistryDistributionPreference } from "../settings.ts"; import type { AcpRegistrySettings } from "../settings.ts"; +import { cliArchiveTarCommand } from "@t3tools/shared/cliRelease"; import * as HostProcess from "@t3tools/shared/HostProcess"; import { mergePathEntries, @@ -238,6 +239,18 @@ export function resolveAcpRegistryPlatformTarget( return os && arch ? (`${os}-${arch}` as AcpRegistryPlatformTarget) : undefined; } +function resolveAcpRegistryPlatformTargetCandidates( + platformTarget: AcpRegistryPlatformTarget | undefined, +): ReadonlyArray { + if (platformTarget === undefined) return []; + // Windows 11 on ARM executes windows-x86_64 binaries under Prism emulation + // when an agent does not publish a native windows-aarch64 binary. + if (platformTarget === "windows-aarch64") { + return ["windows-aarch64", "windows-x86_64"]; + } + return [platformTarget]; +} + interface ExactPackageSpec { readonly name: string; readonly version: string; @@ -375,18 +388,22 @@ export const acpRegistryManagedBinaryDirectories = Effect.fn("acpRegistryManaged (left, right) => right.localeCompare(left, undefined, { numeric: true }), ); for (const version of versions) { - const installRoot = path.join(installsDirectory, agent, version, target); - if (yield* fileSystem.exists(installRoot).pipe(Effect.orElseSucceed(() => false))) { - const binaryTarget = cachedAgentByInstall.get(`${agent}\0${version}`)?.distribution - .binary?.[target]; - const commandSegments = - binaryTarget === undefined ? undefined : normalizeRegistryCommandPath(binaryTarget.cmd); - const directory = - commandSegments === undefined - ? installRoot - : path.join(installRoot, ...commandSegments.slice(0, -1)); - if (yield* fileSystem.exists(directory).pipe(Effect.orElseSucceed(() => false))) { - directories.push(directory); + for (const candidateTarget of resolveAcpRegistryPlatformTargetCandidates(target)) { + const installRoot = path.join(installsDirectory, agent, version, candidateTarget); + if (yield* fileSystem.exists(installRoot).pipe(Effect.orElseSucceed(() => false))) { + const binaryTarget = cachedAgentByInstall.get(`${agent}\0${version}`)?.distribution + .binary?.[candidateTarget]; + const commandSegments = + binaryTarget === undefined + ? undefined + : normalizeRegistryCommandPath(binaryTarget.cmd); + const directory = + commandSegments === undefined + ? installRoot + : path.join(installRoot, ...commandSegments.slice(0, -1)); + if (yield* fileSystem.exists(directory).pipe(Effect.orElseSucceed(() => false))) { + directories.push(directory); + } } } } @@ -409,7 +426,9 @@ export function resolveAcpRegistryDistribution(input: { readonly platformTarget: AcpRegistryPlatformTarget | undefined; }): ResolvedAcpRegistryDistribution | undefined { const { agent, platformTarget } = input; - const binary = platformTarget ? agent.distribution.binary?.[platformTarget] : undefined; + const binary = resolveAcpRegistryPlatformTargetCandidates(platformTarget) + .map((candidate) => agent.distribution.binary?.[candidate]) + .find((entry) => entry !== undefined); const candidates: ReadonlyArray = input.preference === "auto" ? ["binary", "npx", "uvx"] : [input.preference]; @@ -1336,8 +1355,12 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct const binaryPaths = (agent: AcpRegistryAgent, target: typeof AcpRegistryBinaryTarget.Type) => { const commandSegments = normalizeRegistryCommandPath(target.cmd); - if (platformTarget === undefined || commandSegments === undefined) return undefined; - const installRoot = path.join(agentInstallRoot(agent), platformTarget); + const matchedPlatformTarget = + resolveAcpRegistryPlatformTargetCandidates(platformTarget).find( + (candidate) => agent.distribution.binary?.[candidate] === target, + ) ?? platformTarget; + if (matchedPlatformTarget === undefined || commandSegments === undefined) return undefined; + const installRoot = path.join(agentInstallRoot(agent), matchedPlatformTarget); return { commandSegments, installRoot, @@ -1487,6 +1510,7 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct yield* fileSystem.makeDirectory(path.dirname(targetPath), { recursive: true }); yield* fileSystem.rename(archivePath, targetPath); } else { + const tarCommand = cliArchiveTarCommand(platform, hostEnvironment); const listArgs = kind === "tar_gz" ? ["-tzf", archivePath] @@ -1498,7 +1522,7 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct const entries = listArgs === undefined ? yield* runCommand("unzip", ["-Z1", archivePath]) - : yield* runCommand("tar", listArgs); + : yield* runCommand(tarCommand, listArgs); if (!validateArchiveEntries(entries)) { return yield* new AcpRegistryError({ reason: "archive_invalid", @@ -1509,7 +1533,7 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct yield* runCommand("unzip", ["-q", archivePath, "-d", extractionRoot]); } else { const extractFlag = kind === "tar_gz" ? "-xzf" : kind === "tar_bz2" ? "-xjf" : "-xf"; - yield* runCommand("tar", [extractFlag, archivePath, "-C", extractionRoot]); + yield* runCommand(tarCommand, [extractFlag, archivePath, "-C", extractionRoot]); } } From 471074f93dd4e3c57fa61a9f5f85588ecd5da4db Mon Sep 17 00:00:00 2001 From: Max Weinbach Date: Mon, 12 Oct 2026 11:11:08 -0400 Subject: [PATCH 2/3] fix(providers): keep Windows ARM fallback focused on binary selection --- .../provider-acp-registry/src/server/AcpRegistrySupport.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/provider-acp-registry/src/server/AcpRegistrySupport.ts b/packages/provider-acp-registry/src/server/AcpRegistrySupport.ts index 897408c603e4..a1839a1fbf47 100644 --- a/packages/provider-acp-registry/src/server/AcpRegistrySupport.ts +++ b/packages/provider-acp-registry/src/server/AcpRegistrySupport.ts @@ -12,7 +12,6 @@ import { } from "@t3tools/contracts"; import type { AcpRegistryDistributionPreference } from "../settings.ts"; import type { AcpRegistrySettings } from "../settings.ts"; -import { cliArchiveTarCommand } from "@t3tools/shared/cliRelease"; import * as HostProcess from "@t3tools/shared/HostProcess"; import { mergePathEntries, @@ -1510,7 +1509,6 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct yield* fileSystem.makeDirectory(path.dirname(targetPath), { recursive: true }); yield* fileSystem.rename(archivePath, targetPath); } else { - const tarCommand = cliArchiveTarCommand(platform, hostEnvironment); const listArgs = kind === "tar_gz" ? ["-tzf", archivePath] @@ -1522,7 +1520,7 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct const entries = listArgs === undefined ? yield* runCommand("unzip", ["-Z1", archivePath]) - : yield* runCommand(tarCommand, listArgs); + : yield* runCommand("tar", listArgs); if (!validateArchiveEntries(entries)) { return yield* new AcpRegistryError({ reason: "archive_invalid", @@ -1533,7 +1531,7 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct yield* runCommand("unzip", ["-q", archivePath, "-d", extractionRoot]); } else { const extractFlag = kind === "tar_gz" ? "-xzf" : kind === "tar_bz2" ? "-xjf" : "-xf"; - yield* runCommand(tarCommand, [extractFlag, archivePath, "-C", extractionRoot]); + yield* runCommand("tar", [extractFlag, archivePath, "-C", extractionRoot]); } } From 32362f5da45fe60ddb2bd2e50ffcc600aab8a1dd Mon Sep 17 00:00:00 2001 From: Max Weinbach Date: Mon, 12 Oct 2026 11:27:08 -0400 Subject: [PATCH 3/3] fix(providers): extract Windows ARM x64 ZIP fallbacks with bsdtar --- .../src/server/AcpRegistrySupport.test.ts | 86 +++++++++++++++++++ .../src/server/AcpRegistrySupport.ts | 14 ++- 2 files changed, 98 insertions(+), 2 deletions(-) diff --git a/packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts b/packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts index 53628292c193..e4a8126ce564 100644 --- a/packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts +++ b/packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts @@ -14,6 +14,8 @@ import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; import * as Fiber from "effect/Fiber"; import * as Layer from "effect/Layer"; +import * as Sink from "effect/Sink"; +import * as Stream from "effect/Stream"; import * as Schema from "effect/Schema"; import * as HttpClient from "effect/http/HttpClient"; import * as HttpClientResponse from "effect/http/HttpClientResponse"; @@ -823,6 +825,90 @@ describe("AcpRegistrySupport", () => { ); }); + it.effect("prepares an x64 ZIP fallback on Windows ARM when PATH tar cannot read ZIP", () => { + const downloadUrl = "https://registry.test/agent-windows-x64.zip"; + const agent = makeAgent({ + binary: { "windows-x86_64": { archive: downloadUrl, cmd: "./agent.exe" } }, + npx: { package: "@example/acp@1.2.3" }, + }); + return Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const cacheDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-acp-arm-zip-" }); + const commands: Array = []; + const spawner = ChildProcessSpawner.make((command) => + Effect.gen(function* () { + if (command._tag !== "StandardCommand") return yield* Effect.die("Unexpected pipeline"); + commands.push(command.command); + // Simulate Git's GNU tar rejecting ZIP and System32 bsdtar listing/extracting it. + const supportsZip = command.command === "C:\\Windows\\System32\\tar.exe"; + if (supportsZip && command.args[0] === "-xf") { + const outputDirectory = command.args[3]; + if (!outputDirectory) return yield* Effect.die("Missing extraction directory"); + yield* fileSystem.writeFileString(path.join(outputDirectory, "agent.exe"), "fixture"); + } + return ChildProcessSpawner.makeHandle({ + pid: ChildProcessSpawner.ProcessId(1), + exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(supportsZip ? 0 : 1)), + isRunning: Effect.succeed(false), + kill: () => Effect.void, + unref: Effect.succeed(Effect.void), + stdin: Sink.drain, + stdout: Stream.make(new TextEncoder().encode(supportsZip ? "agent.exe\r\n" : "")), + stderr: Stream.make( + new TextEncoder().encode(supportsZip ? "" : "GNU tar cannot read ZIP"), + ), + all: Stream.empty, + getInputFd: () => Sink.drain, + getOutputFd: () => Stream.empty, + }); + }).pipe(Effect.orDie), + ); + const resolver = yield* AcpRegistrySupport.makeAcpRegistryCatalog({ + cacheDir, + toolsDir: path.join(cacheDir, "tools"), + registryUrl, + }).pipe( + Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), + Effect.provideService(HostProcess.Platform, "win32"), + Effect.provideService(HostProcess.Architecture, "arm64"), + Effect.provideService(HostProcess.Environment, { + SystemRoot: "C:\\Windows", + PATH: "C:\\Program Files\\Git\\usr\\bin;C:\\Windows\\System32", + }), + Effect.provideService(SpawnExecutableResolution, (command) => + command === "tar" ? "C:\\Program Files\\Git\\usr\\bin\\tar.exe" : command, + ), + ); + yield* resolver.prepare({ agentId: agent.id }); + const resolved = yield* resolver.resolve(settings(), "/workspace", {}); + expect(resolved.spawn.command).toBe( + yield* fileSystem.realPath( + path.join(cacheDir, "tools", agent.id, agent.version, "windows-x86_64", "agent.exe"), + ), + ); + expect(yield* fileSystem.readFileString(resolved.spawn.command)).toBe("fixture"); + expect(commands).toEqual([ + "C:\\Windows\\System32\\tar.exe", + "C:\\Windows\\System32\\tar.exe", + ]); + }).pipe( + Effect.scoped, + Effect.provide( + layerResolver((request) => + Effect.succeed( + HttpClientResponse.fromWeb( + request, + request.url === registryUrl + ? new Response(makeRegistry(agent)) + : new Response("ZIP fixture"), + ), + ), + ), + ), + ); + }); + it.effect("searches compatible agents with deterministic ranking and bounded metadata", () => { const exact = { ...makeAgent({ npx: { package: "@example/acp@1.2.3" } }), diff --git a/packages/provider-acp-registry/src/server/AcpRegistrySupport.ts b/packages/provider-acp-registry/src/server/AcpRegistrySupport.ts index a1839a1fbf47..1011141229bf 100644 --- a/packages/provider-acp-registry/src/server/AcpRegistrySupport.ts +++ b/packages/provider-acp-registry/src/server/AcpRegistrySupport.ts @@ -12,6 +12,7 @@ import { } from "@t3tools/contracts"; import type { AcpRegistryDistributionPreference } from "../settings.ts"; import type { AcpRegistrySettings } from "../settings.ts"; +import { cliArchiveTarCommand } from "@t3tools/shared/cliRelease"; import * as HostProcess from "@t3tools/shared/HostProcess"; import { mergePathEntries, @@ -1363,6 +1364,7 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct return { commandSegments, installRoot, + platformTarget: matchedPlatformTarget, executablePath: path.join(installRoot, ...commandSegments), }; }; @@ -1509,6 +1511,14 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct yield* fileSystem.makeDirectory(path.dirname(targetPath), { recursive: true }); yield* fileSystem.rename(archivePath, targetPath); } else { + // The new Windows ARM fallback must extract x64 ZIPs even when Git's GNU tar wins PATH. + // Keep archive selection for existing native distributions unchanged in this PR. + const tarCommand = + platformTarget === "windows-aarch64" && + paths.platformTarget === "windows-x86_64" && + kind === "zip" + ? cliArchiveTarCommand(platform, hostEnvironment) + : "tar"; const listArgs = kind === "tar_gz" ? ["-tzf", archivePath] @@ -1520,7 +1530,7 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct const entries = listArgs === undefined ? yield* runCommand("unzip", ["-Z1", archivePath]) - : yield* runCommand("tar", listArgs); + : yield* runCommand(tarCommand, listArgs); if (!validateArchiveEntries(entries)) { return yield* new AcpRegistryError({ reason: "archive_invalid", @@ -1531,7 +1541,7 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct yield* runCommand("unzip", ["-q", archivePath, "-d", extractionRoot]); } else { const extractFlag = kind === "tar_gz" ? "-xzf" : kind === "tar_bz2" ? "-xjf" : "-xf"; - yield* runCommand("tar", [extractFlag, archivePath, "-C", extractionRoot]); + yield* runCommand(tarCommand, [extractFlag, archivePath, "-C", extractionRoot]); } }