fix(runner): bundle the Claude ACP runtime and explain missing packages

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-11 14:43:49 -05:00
1 parent bfe7c6357d
commit 2d81789c14
19 files changed
+180 -9

No files matched your search

@@ -100,6 +100,21 @@ export async function findRunnerExternalPackages(entryPoints) {
return externalPackageNames;
}
// Provider entrypoints are resolved with createRequire from profile metadata,
// so esbuild cannot see them. These two bridges ship in the server package;
// other ACP agents use separately provisioned provider packs.
export async function findRunnerRuntimePackages(entryPoints, profileData) {
const packages = await findRunnerExternalPackages(entryPoints);
for (const agent of ["claude", "codex"]) {
const profile = profileData?.profiles?.[agent];
if (profileData?.schema !== "paperclip.acpx-profiles.v1" || !profile?.agentServerPackage) {
throw new Error(`paperclip-runner vendor check: missing ${agent} provider profile.`);
}
packages.add(profile.agentServerPackage);
}
return packages;
}
function readDependencyNames(packageJsonPath) {
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
return new Map(Object.entries(packageJson.dependencies ?? {}));
@@ -125,7 +140,8 @@ function explainMissingDependencies(missing, runnerDependencyNames) {
async function main() {
const entryPoints = ENTRY_POINT_NAMES.map((name) => resolve(runnerDist, name));
const externalPackageNames = await findRunnerExternalPackages(entryPoints);
const profileData = JSON.parse(readFileSync(resolve(runnerRoot, "acpx-profiles.json"), "utf8"));
const externalPackageNames = await findRunnerRuntimePackages(entryPoints, profileData);
const serverDependencyNames = readDependencyNames(resolve(serverRoot, "package.json"));
const missing = findMissingVendorDependencies(
@@ -1,4 +1,4 @@
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
@@ -7,6 +7,7 @@ import { afterEach, describe, expect, it } from "vitest";
import {
findMissingVendorDependencies,
findRunnerExternalPackages,
findRunnerRuntimePackages,
} from "./verify-runner-vendor-dependencies.mjs";
describe("findMissingVendorDependencies", () => {
@@ -87,6 +88,15 @@ describe("findRunnerExternalPackages", () => {
expect(externalPackageNames).toEqual(new Set(["smol-toml"]));
});
it("checks dynamically resolved bundled providers even without a JavaScript import", async () => {
const entryPoints = writeFixture();
const profiles = JSON.parse(readFileSync(new URL("../../packages/paperclip-runner/acpx-profiles.json", import.meta.url), "utf8"));
const packages = await findRunnerRuntimePackages(entryPoints, profiles);
expect(findMissingVendorDependencies(packages, new Set(["smol-toml", "@agentclientprotocol/codex-acp"])))
.toEqual(["@agentclientprotocol/claude-agent-acp"]);
expect(packages.has("pi-acp")).toBe(false);
});
it("throws a clear, actionable error when an entry point is missing", async () => {
fixtureDir = mkdtempSync(join(tmpdir(), "paperclip-runner-vendor-fixture-"));
const missingEntryPoint = join(fixtureDir, "index.js");
@@ -96,3 +106,12 @@ describe("findRunnerExternalPackages", () => {
);
});
});
// Claude is resolved through createRequire at runtime, outside esbuild's graph.
it("ships the qualified Claude ACP server with the vendored runner", () => {
const server = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
const profiles = JSON.parse(readFileSync(new URL("../../packages/paperclip-runner/acpx-profiles.json", import.meta.url), "utf8"));
const claude = profiles.profiles.claude;
expect(server.dependencies[claude.agentServerPackage]).toBe(claude.agentServerVersion);
expect(server.bundleDependencies).toContain(claude.agentServerPackage);
});