Skip to content
Merged
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
127 changes: 127 additions & 0 deletions packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -209,6 +211,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", () => {
Expand Down Expand Up @@ -782,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<string> = [];
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" } }),
Expand Down
66 changes: 49 additions & 17 deletions packages/provider-acp-registry/src/server/AcpRegistrySupport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -238,6 +239,18 @@ export function resolveAcpRegistryPlatformTarget(
return os && arch ? (`${os}-${arch}` as AcpRegistryPlatformTarget) : undefined;
}

function resolveAcpRegistryPlatformTargetCandidates(
platformTarget: AcpRegistryPlatformTarget | undefined,
): ReadonlyArray<AcpRegistryPlatformTarget> {
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"];
Comment thread
mweinbach marked this conversation as resolved.
}
return [platformTarget];
}

interface ExactPackageSpec {
readonly name: string;
readonly version: string;
Expand Down Expand Up @@ -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);
}
}
}
}
Expand All @@ -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);
Comment thread
mweinbach marked this conversation as resolved.
const candidates: ReadonlyArray<AcpRegistryDistributionKind> =
input.preference === "auto" ? ["binary", "npx", "uvx"] : [input.preference];

Expand Down Expand Up @@ -1336,11 +1355,16 @@ 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,
platformTarget: matchedPlatformTarget,
executablePath: path.join(installRoot, ...commandSegments),
};
};
Expand Down Expand Up @@ -1487,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]
Expand All @@ -1498,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",
Expand All @@ -1509,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]);
}
}

Expand Down
Loading