From 40830f61e8659ba91d00fece9f3b2657a3f97481 Mon Sep 17 00:00:00 2001 From: Dotta Date: Thu, 3 Sep 2026 02:52:10 -0500 Subject: [PATCH] fix(runner): resolve exported ACP package manifests --- .../acpx/installation-integrity.test.ts | 19 ++++++++- .../drivers/acpx/installation-integrity.ts | 40 ++++++++++++++++++- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/packages/paperclip-runner/src/drivers/acpx/installation-integrity.test.ts b/packages/paperclip-runner/src/drivers/acpx/installation-integrity.test.ts index 1e7fcbdacc..4a808a6969 100644 --- a/packages/paperclip-runner/src/drivers/acpx/installation-integrity.test.ts +++ b/packages/paperclip-runner/src/drivers/acpx/installation-integrity.test.ts @@ -82,8 +82,13 @@ describe("ACPX installation integrity", () => { await mkdir(nestedDependencyDirectory, { recursive: true }); await writeFile( nestedDependencyPackageJson, - JSON.stringify({ name: "qualified-dependency", version: "1.0.0" }), + JSON.stringify({ + name: "qualified-dependency", + version: "1.0.0", + exports: "./index.js", + }), ); + await writeFile(join(nestedDependencyDirectory, "index.js"), "export {};"); expect( createAcpxPackageJsonResolver(root)( "qualified-dependency", @@ -549,6 +554,18 @@ describe("ACPX installation integrity", () => { ); }); + it.runIf(process.platform === "linux" && process.arch === "x64")( + "resolves and pins the installed Claude ACP dependency graph", + async () => { + const profile = resolveQualifiedAcpxProfile("claude", "claude-sonnet-5"); + const installation = await verifyQualifiedAcpxInstallation(profile); + expect(installation.agentServerPackageJsonPath).toContain( + "/@agentclientprotocol/claude-agent-acp/package.json", + ); + await (await installation.openCommand()).close(); + }, + ); + it.runIf(process.platform === "linux" && process.arch === "x64")( "resolves and pins the qualified Codex native runtime through its transitive packages", async () => { diff --git a/packages/paperclip-runner/src/drivers/acpx/installation-integrity.ts b/packages/paperclip-runner/src/drivers/acpx/installation-integrity.ts index 553e1360b5..e5a8fcfaf3 100644 --- a/packages/paperclip-runner/src/drivers/acpx/installation-integrity.ts +++ b/packages/paperclip-runner/src/drivers/acpx/installation-integrity.ts @@ -295,7 +295,7 @@ export function createAcpxPackageJsonResolver( ); } const packageJsonPath = realpathSync( - createRequire(canonicalIssuer).resolve(`${packageName}/package.json`), + resolvePackageJsonFromIssuer(packageName, canonicalIssuer), ); if (!pathIsInside(canonicalNodeModules, packageJsonPath)) { throw new Error( @@ -306,6 +306,44 @@ export function createAcpxPackageJsonResolver( }; } +function resolvePackageJsonFromIssuer( + packageName: string, + issuerPackageJsonPath: string, +): string { + const issuerRequire = createRequire(issuerPackageJsonPath); + try { + return issuerRequire.resolve(`${packageName}/package.json`); + } catch (error) { + if ( + (error as NodeJS.ErrnoException).code !== "ERR_PACKAGE_PATH_NOT_EXPORTED" + ) + throw error; + } + + const packageSegments = packageName.split("/"); + if ( + packageSegments.length < 1 || + packageSegments.length > 2 || + packageSegments.some((segment) => segment.length === 0) + ) { + throw new Error(`ACPX provider package name is invalid: ${packageName}`); + } + let directory = dirname(realpathSync(issuerRequire.resolve(packageName))); + for (let count = 0; count < MAX_DEPENDENCY_ANCESTORS; count += 1) { + const matchesPackage = + basename(directory) === packageSegments.at(-1) && + (packageSegments.length === 1 || + basename(dirname(directory)) === packageSegments[0]); + if (matchesPackage) return resolve(directory, "package.json"); + const parent = dirname(directory); + if (parent === directory) break; + directory = parent; + } + throw new Error( + `ACPX provider package manifest could not be located for ${packageName}`, + ); +} + function pathIsInside(root: string, candidate: string): boolean { const candidateRelativePath = relative(root, candidate); return (