From ea796dc454da16c55c59de678803eb8785e13a2b Mon Sep 17 00:00:00 2001 From: Kareem Abdo <142717280+kareemsetl@users.noreply.github.com> Date: Mon, 26 Oct 2026 11:26:43 -0400 Subject: [PATCH 1/2] fix(provider-acp-registry): install agents with large archive listings Registry binary installs list the archive before extracting it so unsafe paths can be rejected. That listing was read through the generic 1 MiB command output cap, but listing size tracks entry count rather than archive size: Cortex Code's macOS build has 12,752 entries and a 1.5 MB listing, so installing it always failed with "produced more output than expected". Give archive listings their own 64 MiB cap. Extraction and package-manager commands keep the 1 MiB cap. --- .../src/server/AcpRegistrySupport.test.ts | 65 +++++++++++++++++++ .../src/server/AcpRegistrySupport.ts | 17 +++-- 2 files changed, 78 insertions(+), 4 deletions(-) diff --git a/packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts b/packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts index 7b11b520ea999..1fe09d45ce925 100644 --- a/packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts +++ b/packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts @@ -755,6 +755,71 @@ describe("AcpRegistrySupport", () => { ); }); + it.effect("installs an archive whose entry listing exceeds the command output limit", () => { + const downloadUrl = "https://registry.test/agent-long-listing.tar.gz"; + const agent = makeAgent({ + binary: { "linux-x86_64": { archive: downloadUrl, cmd: "./agent" } }, + }); + return Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const cacheDir = yield* fileSystem.makeTempDirectoryScoped({ + prefix: "t3-acp-long-listing-", + }); + const source = `${cacheDir}/source`; + yield* fileSystem.makeDirectory(source); + yield* fileSystem.writeFileString(`${source}/agent`, "#!/bin/sh\necho managed\n"); + // 5,000 long names list past the generic 1 MiB command output cap. + const entries = `${source}/entries`; + yield* fileSystem.makeDirectory(entries); + yield* Effect.forEach( + Array.from({ length: 5_000 }, (_, index) => index), + (index) => fileSystem.writeFileString(`${entries}/${index}-${"e".repeat(240)}`, ""), + { concurrency: 16 }, + ); + const archivePath = `${cacheDir}/agent.tar.gz`; + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const tar = yield* spawner.spawn( + ChildProcess.make("tar", ["-czf", archivePath, "-C", source, "."]), + ); + expect(yield* tar.exitCode).toBe(0); + const archive = yield* fileSystem.readFile(archivePath); + const resolver = yield* AcpRegistrySupport.makeAcpRegistryCatalog({ + cacheDir, + toolsDir: `${cacheDir}/tools`, + registryUrl, + }).pipe( + Effect.provideService( + HttpClient.HttpClient, + HttpClient.make((request) => + Effect.succeed( + HttpClientResponse.fromWeb( + request, + request.url === registryUrl + ? new Response(makeRegistry(agent)) + : new Response(archive), + ), + ), + ), + ), + ); + yield* resolver.prepare({ agentId: agent.id }); + const resolved = yield* resolver.resolve(settings(), "/workspace", {}); + expect(yield* fileSystem.readFileString(resolved.spawn.command)).toBe( + "#!/bin/sh\necho managed\n", + ); + }).pipe( + Effect.scoped, + Effect.provide( + Layer.mergeAll( + NodeServices.layer, + TestProviderHost.layer().pipe(Layer.provide(NodeServices.layer)), + ), + ), + Effect.provideService(HostProcess.Platform, "linux"), + Effect.provideService(HostProcess.Architecture, "x64"), + ); + }); + 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 d97c110d8611e..eb9236e8af552 100644 --- a/packages/provider-acp-registry/src/server/AcpRegistrySupport.ts +++ b/packages/provider-acp-registry/src/server/AcpRegistrySupport.ts @@ -51,6 +51,10 @@ const ACP_REGISTRY_URL = "https://cdn.agentclientprotocol.com/registry/v1/latest const MAX_REGISTRY_BYTES = 1024 * 1024; const MAX_ARCHIVE_BYTES = 1024 * 1024 * 1024; +const MAX_COMMAND_OUTPUT_BYTES = 1024 * 1024; +// Listings scale with entry count, not archive size: Cortex Code's macOS build lists +// 12,752 entries in 1.5 MB. +const MAX_ARCHIVE_LISTING_BYTES = 64 * 1024 * 1024; const MAX_PACKAGE_MANIFEST_BYTES = 1024 * 1024; // Older clients omit the limit and validate responses against a 20-agent cap. const DEFAULT_SEARCH_LIMIT = 20; @@ -894,6 +898,7 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct readonly env?: NodeJS.ProcessEnv; readonly timeout?: Duration.Input; readonly truncatedOutputReason?: AcpRegistryErrorReason; + readonly maxStdoutBytes?: number; } = {}, ) { const collect = Effect.gen(function* () { @@ -912,8 +917,11 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct yield* Effect.addFinalizer(() => child.kill().pipe(Effect.ignore)); const [stdout, stderr, exitCode] = yield* Effect.all( [ - collectUint8StreamText({ stream: child.stdout, maxBytes: 1024 * 1024 }), - collectUint8StreamText({ stream: child.stderr, maxBytes: 1024 * 1024 }), + collectUint8StreamText({ + stream: child.stdout, + maxBytes: options.maxStdoutBytes ?? MAX_COMMAND_OUTPUT_BYTES, + }), + collectUint8StreamText({ stream: child.stderr, maxBytes: MAX_COMMAND_OUTPUT_BYTES }), child.exitCode, ], { concurrency: "unbounded" }, @@ -1510,10 +1518,11 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct : platform === "win32" ? ["-tf", archivePath] : undefined; + const listingOptions = { maxStdoutBytes: MAX_ARCHIVE_LISTING_BYTES }; const entries = listArgs === undefined - ? yield* runCommand("unzip", ["-Z1", archivePath]) - : yield* runCommand(tarCommand, listArgs); + ? yield* runCommand("unzip", ["-Z1", archivePath], listingOptions) + : yield* runCommand(tarCommand, listArgs, listingOptions); if (!validateArchiveEntries(entries)) { return yield* new AcpRegistryError({ reason: "archive_invalid", From b9fdb5ba45eb3aa7df4649658183443f8df98fec Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Tue, 27 Oct 2026 01:04:59 +0000 Subject: [PATCH 2/2] test(provider-acp-registry): reuse archive install coverage for large listings --- .../src/server/AcpRegistrySupport.test.ts | 65 ++----------------- .../src/server/AcpRegistrySupport.ts | 3 +- 2 files changed, 5 insertions(+), 63 deletions(-) diff --git a/packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts b/packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts index 1fe09d45ce925..05941a59baf4c 100644 --- a/packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts +++ b/packages/provider-acp-registry/src/server/AcpRegistrySupport.test.ts @@ -697,7 +697,7 @@ describe("AcpRegistrySupport", () => { }, ); - it.effect("installs a tar archive containing a root directory entry", () => { + it.effect("installs a tar archive with a root entry and a listing larger than 1 MiB", () => { const downloadUrl = "https://registry.test/agent.tar.gz"; const agent = makeAgent({ binary: { "linux-x86_64": { archive: downloadUrl, cmd: "./agent" } }, @@ -708,66 +708,6 @@ describe("AcpRegistrySupport", () => { const source = `${cacheDir}/source`; yield* fileSystem.makeDirectory(source); yield* fileSystem.writeFileString(`${source}/agent`, "#!/bin/sh\necho managed\n"); - const archivePath = `${cacheDir}/agent.tar.gz`; - const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; - const tar = yield* spawner.spawn( - ChildProcess.make("tar", ["-czf", archivePath, "-C", source, "."]), - ); - expect(yield* tar.exitCode).toBe(0); - const archive = yield* fileSystem.readFile(archivePath); - const resolver = yield* AcpRegistrySupport.makeAcpRegistryCatalog({ - cacheDir, - toolsDir: `${cacheDir}/tools`, - registryUrl, - }).pipe( - Effect.provideService( - HttpClient.HttpClient, - HttpClient.make((request) => - Effect.succeed( - HttpClientResponse.fromWeb( - request, - request.url === registryUrl - ? new Response(makeRegistry(agent)) - : new Response(archive), - ), - ), - ), - ), - ); - yield* resolver.prepare({ agentId: agent.id }); - const resolved = yield* resolver.resolve(settings(), "/workspace", {}); - expect(resolved.spawn.command).toBe( - yield* fileSystem.realPath(`${cacheDir}/tools/example-agent/1.2.3/linux-x86_64/agent`), - ); - expect(yield* fileSystem.readFileString(resolved.spawn.command)).toBe( - "#!/bin/sh\necho managed\n", - ); - }).pipe( - Effect.scoped, - Effect.provide( - Layer.mergeAll( - NodeServices.layer, - TestProviderHost.layer().pipe(Layer.provide(NodeServices.layer)), - ), - ), - Effect.provideService(HostProcess.Platform, "linux"), - Effect.provideService(HostProcess.Architecture, "x64"), - ); - }); - - it.effect("installs an archive whose entry listing exceeds the command output limit", () => { - const downloadUrl = "https://registry.test/agent-long-listing.tar.gz"; - const agent = makeAgent({ - binary: { "linux-x86_64": { archive: downloadUrl, cmd: "./agent" } }, - }); - return Effect.gen(function* () { - const fileSystem = yield* FileSystem.FileSystem; - const cacheDir = yield* fileSystem.makeTempDirectoryScoped({ - prefix: "t3-acp-long-listing-", - }); - const source = `${cacheDir}/source`; - yield* fileSystem.makeDirectory(source); - yield* fileSystem.writeFileString(`${source}/agent`, "#!/bin/sh\necho managed\n"); // 5,000 long names list past the generic 1 MiB command output cap. const entries = `${source}/entries`; yield* fileSystem.makeDirectory(entries); @@ -804,6 +744,9 @@ describe("AcpRegistrySupport", () => { ); yield* resolver.prepare({ agentId: agent.id }); const resolved = yield* resolver.resolve(settings(), "/workspace", {}); + expect(resolved.spawn.command).toBe( + yield* fileSystem.realPath(`${cacheDir}/tools/example-agent/1.2.3/linux-x86_64/agent`), + ); expect(yield* fileSystem.readFileString(resolved.spawn.command)).toBe( "#!/bin/sh\necho managed\n", ); diff --git a/packages/provider-acp-registry/src/server/AcpRegistrySupport.ts b/packages/provider-acp-registry/src/server/AcpRegistrySupport.ts index eb9236e8af552..d9df0e0effa6c 100644 --- a/packages/provider-acp-registry/src/server/AcpRegistrySupport.ts +++ b/packages/provider-acp-registry/src/server/AcpRegistrySupport.ts @@ -52,8 +52,7 @@ const ACP_REGISTRY_URL = "https://cdn.agentclientprotocol.com/registry/v1/latest const MAX_REGISTRY_BYTES = 1024 * 1024; const MAX_ARCHIVE_BYTES = 1024 * 1024 * 1024; const MAX_COMMAND_OUTPUT_BYTES = 1024 * 1024; -// Listings scale with entry count, not archive size: Cortex Code's macOS build lists -// 12,752 entries in 1.5 MB. +// Archive listings can exceed the output of package-manager commands. const MAX_ARCHIVE_LISTING_BYTES = 64 * 1024 * 1024; const MAX_PACKAGE_MANIFEST_BYTES = 1024 * 1024; // Older clients omit the limit and validate responses against a 20-agent cap.