mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-29 04:25:52 +01:00
fix: stage complete installed Codex runtime dependencies
Preserve prepared Git installation assets and consumer lifecycle behavior. Bundle the qualified Codex graph and host artifacts without changing agent defaults. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
71cd0a2621
commit
5fa4abaead
10 files changed
+1209
-19
No files matched your search
@@ -1,12 +1,15 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
existsSync,
|
||||
lstatSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
realpathSync,
|
||||
rmSync,
|
||||
symlinkSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { readFile } from "node:fs/promises";
|
||||
@@ -20,11 +23,58 @@ import cliEsbuildConfig from "../cli/esbuild.config.mjs";
|
||||
import { bundledCliNpmDependencies } from "./cli-bundled-npm-dependencies.mjs";
|
||||
import {
|
||||
createBundledInstallManifest,
|
||||
configureBundledProviderOverrides,
|
||||
dockerBundledProviderTarget,
|
||||
inspectBundledNativeArtifact,
|
||||
materializePublishManifest,
|
||||
mergeBundledProviderGraph,
|
||||
selectBundledDependencyPatches,
|
||||
stageBundledEsbuildOptionalDependencies,
|
||||
stageBundledProviderOptionalDependencies,
|
||||
} from "./prepare-bundled-package.mjs";
|
||||
|
||||
const rootPackage = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
|
||||
const profileData = JSON.parse(await readFile(new URL("../packages/paperclip-runner/acpx-profiles.json", import.meta.url), "utf8"));
|
||||
const runtimeData = JSON.parse(await readFile(new URL("../packages/paperclip-runner/src/drivers/acpx/qualified-runtime-artifacts.json", import.meta.url), "utf8"));
|
||||
const nativeTargets = ["linux-x64", "darwin-arm64", "darwin-x64"];
|
||||
function esbuildFixture(t) {
|
||||
const directory = realpathSync(mkdtempSync(join(tmpdir(), "paperclip-bundled-esbuild-")));
|
||||
t.after(() => rmSync(directory, { recursive: true, force: true }));
|
||||
const graph = join(directory, "node_modules");
|
||||
const writePackage = (location, metadata) => {
|
||||
mkdirSync(location, { recursive: true });
|
||||
writeFileSync(join(location, "package.json"), JSON.stringify(metadata));
|
||||
return location;
|
||||
};
|
||||
const acpx = writePackage(join(graph, "acpx"), { name: "acpx", version: "0.13.1" });
|
||||
const esbuild = writePackage(join(acpx, "node_modules/esbuild"), {
|
||||
name: "esbuild", version: "0.28.2", scripts: { postinstall: "node install.js" },
|
||||
optionalDependencies: Object.fromEntries([...nativeTargets, "win32-x64"].map(target => [`@esbuild/${target}`, "0.28.2"])),
|
||||
});
|
||||
const producerBinary = writePackage(join(graph, "@esbuild/linux-x64"), {
|
||||
name: "@esbuild/linux-x64", version: "0.28.2", os: ["linux"], cpu: ["x64"],
|
||||
});
|
||||
writePackage(join(graph, "unrelated-linux-x64"), { name: "unrelated-linux-x64", version: "1.0.0" });
|
||||
const publish = { name: "@paperclipai/adapter-utils", version: "0.3.1", bundleDependencies: ["acpx"],
|
||||
dependencies: { acpx: "0.13.1" }, optionalDependencies: { unrelated: "1.0.0" } };
|
||||
return { directory, graph, esbuild, producerBinary, publish, writePackage };
|
||||
}
|
||||
function nativeFixture(directory, profile, target, data) {
|
||||
const [os, cpu] = target.split("-");
|
||||
const name = `${profile.agentRuntimePackage}-${target}`;
|
||||
const packageRoot = join(directory, name);
|
||||
mkdirSync(packageRoot, { recursive: true });
|
||||
writeFileSync(join(packageRoot, "package.json"), JSON.stringify({ name, version: `${profile.agentRuntimeVersion}-${target}`, os: [os], cpu: [cpu] }));
|
||||
const qualification = data[profile.agent].platforms[target];
|
||||
const executable = qualification?.relativeExecutable ?? `vendor/${cpu === "arm64" ? "aarch64" : "x86_64"}-apple-darwin/bin/codex`;
|
||||
const bytes = Buffer.alloc(32);
|
||||
if (os === "linux") { bytes.set([0x7f, 0x45, 0x4c, 0x46, 2, 1]); bytes.writeUInt16LE(62, 18); }
|
||||
else { bytes.writeUInt32LE(0xfeedfacf, 0); bytes.writeUInt32LE(cpu === "arm64" ? 0x0100000c : 0x01000007, 4); }
|
||||
mkdirSync(join(packageRoot, executable, ".."), { recursive: true });
|
||||
writeFileSync(join(packageRoot, executable), bytes, { mode: 0o755 });
|
||||
if (qualification) qualification.executableDigest = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
||||
return packageRoot;
|
||||
}
|
||||
const adapterUtilsPackage = JSON.parse(
|
||||
await readFile(new URL("../packages/adapter-utils/package.json", import.meta.url), "utf8"),
|
||||
);
|
||||
@@ -40,6 +90,7 @@ const dbPackage = JSON.parse(
|
||||
const releaseScript = await readFile(new URL("./release.sh", import.meta.url), "utf8");
|
||||
const releaseLib = await readFile(new URL("./release-lib.sh", import.meta.url), "utf8");
|
||||
const buildNpmScript = await readFile(new URL("./build-npm.sh", import.meta.url), "utf8");
|
||||
|
||||
const acpxRuntimePatch = await readFile(
|
||||
new URL("../patches/acpx@0.13.1.patch", import.meta.url),
|
||||
"utf8",
|
||||
@@ -124,6 +175,186 @@ test("published packages preserve the patched ACPX runtime", () => {
|
||||
assert.equal(cliEsbuildConfig.external.includes("acpx"), false);
|
||||
});
|
||||
|
||||
test("published Codex bridge uses its qualified producer closure without consumer overrides", () => {
|
||||
const published = materializePublishManifest(serverPackage);
|
||||
const { installManifest, profiles } = configureBundledProviderOverrides(
|
||||
createBundledInstallManifest(published, serverPackage.bundleDependencies),
|
||||
serverPackage.bundleDependencies, rootPackage, profileData,
|
||||
);
|
||||
assert.equal(profiles.length, 1);
|
||||
for (const profile of profiles) {
|
||||
assert.equal(published.dependencies[profile.agentServerPackage], profile.agentServerVersion);
|
||||
assert.ok(published.bundleDependencies.includes(profile.agentServerPackage));
|
||||
assert.equal(installManifest.overrides[`${profile.agentServerPackage}@${profile.agentServerVersion}`][profile.agentRuntimePackage], profile.agentRuntimeVersion);
|
||||
}
|
||||
assert.equal(published.overrides, undefined);
|
||||
assert.equal(installManifest.overrides.rollup, undefined);
|
||||
const bad = structuredClone(rootPackage);
|
||||
bad.pnpm.overrides["@agentclientprotocol/codex-acp@1.6.2>@openai/codex"] = "0.156.1";
|
||||
assert.throws(() => configureBundledProviderOverrides(installManifest, serverPackage.bundleDependencies, bad, profileData), /runtime override must match/);
|
||||
const unpinned = createBundledInstallManifest(published, serverPackage.bundleDependencies);
|
||||
unpinned.dependencies["@agentclientprotocol/codex-acp"] = "^1.6.2";
|
||||
assert.throws(() => configureBundledProviderOverrides(unpinned, serverPackage.bundleDependencies, rootPackage, profileData), /exact qualified profile/);
|
||||
const conflict = createBundledInstallManifest(published, serverPackage.bundleDependencies);
|
||||
conflict.overrides = { "@agentclientprotocol/codex-acp@1.6.2": { "@openai/codex": "0.156.1" } };
|
||||
assert.throws(() => configureBundledProviderOverrides(conflict, serverPackage.bundleDependencies, rootPackage, profileData), /conflicting producer override/);
|
||||
});
|
||||
|
||||
test("Linux-produced Codex bridge tarballs retain all three contained artifacts and preserve runtime declarations", (t) => {
|
||||
const destination = mkdtempSync(join(tmpdir(), "paperclip-bundled-provider-"));
|
||||
t.after(() => rmSync(destination, { recursive: true, force: true }));
|
||||
const graph = join(destination, "node_modules");
|
||||
const artifacts = join(destination, "artifacts"), data = structuredClone(runtimeData);
|
||||
mkdirSync(artifacts);
|
||||
const { profiles } = configureBundledProviderOverrides(
|
||||
createBundledInstallManifest(serverPackage, serverPackage.bundleDependencies), serverPackage.bundleDependencies, rootPackage, profileData,
|
||||
);
|
||||
const writePackage = (name, metadata) => {
|
||||
const directory = join(graph, name);
|
||||
mkdirSync(directory, { recursive: true });
|
||||
writeFileSync(join(directory, "package.json"), JSON.stringify({ name, ...metadata }));
|
||||
return directory;
|
||||
};
|
||||
for (const profile of profiles) {
|
||||
writePackage(profile.agentServerPackage, { version: profile.agentServerVersion,
|
||||
dependencies: { [profile.agentRuntimePackage]: profile.agentRuntimeVersion } });
|
||||
const optional = Object.fromEntries(nativeTargets.map(target => [`${profile.agentRuntimePackage}-${target}`,
|
||||
`npm:@openai/codex@${profile.agentRuntimeVersion}-${target}`]));
|
||||
writePackage(profile.agentRuntimePackage, { version: profile.agentRuntimeVersion, optionalDependencies: optional });
|
||||
writePackage(`${profile.agentRuntimePackage}-linux-x64`, { version: "0.160.0-linux-x64" });
|
||||
for (const target of nativeTargets) nativeFixture(artifacts, profile, target, data);
|
||||
}
|
||||
writePackage("unrelated-linux-x64", { version: "1.0.0" });
|
||||
const before = readFileSync(join(graph, "@agentclientprotocol/codex-acp/package.json"), "utf8");
|
||||
const stage = manifest => stageBundledProviderOptionalDependencies(destination, manifest, profiles, data, { nativeArtifactsDirectory: artifacts });
|
||||
const staged = stage(serverPackage);
|
||||
assert.equal(staged.overrides, undefined);
|
||||
assert.equal(staged.optionalDependencies["@openai/codex-darwin-arm64"], "npm:@openai/codex@0.160.0-darwin-arm64");
|
||||
for (const profile of profiles) {
|
||||
for (const target of nativeTargets) {
|
||||
assert.equal(existsSync(join(graph, `${profile.agentRuntimePackage}-${target}`)), true);
|
||||
assert.ok(staged.bundleDependencies.includes(`${profile.agentRuntimePackage}-${target}`));
|
||||
}
|
||||
assert.equal(existsSync(join(graph, profile.agentRuntimePackage, "package.json")), true);
|
||||
}
|
||||
assert.equal(existsSync(join(graph, "unrelated-linux-x64")), true);
|
||||
assert.equal(readFileSync(join(graph, "@agentclientprotocol/codex-acp/package.json"), "utf8"), before);
|
||||
const sdkPath = join(graph, "@openai/codex/package.json");
|
||||
const sdk = JSON.parse(readFileSync(sdkPath, "utf8"));
|
||||
writeFileSync(sdkPath, JSON.stringify({ ...sdk, version: "0.156.1" }));
|
||||
assert.throws(() => stage(serverPackage), /runtime version mismatch/);
|
||||
writeFileSync(sdkPath, JSON.stringify(sdk));
|
||||
assert.throws(() => stage({ ...serverPackage,
|
||||
optionalDependencies: { "@openai/codex-darwin-arm64": "npm:@openai/codex@0.156.1-darwin-arm64" } }), /conflicts with published/);
|
||||
writeFileSync(sdkPath, JSON.stringify({ ...sdk, optionalDependencies: { "@openai/codex-untrusted": "0.160.0" } }));
|
||||
assert.throws(() => stage(serverPackage), /not qualified/);
|
||||
writeFileSync(sdkPath, JSON.stringify(sdk));
|
||||
const codex = profiles.find(value => value.agent === "codex");
|
||||
const artifact = join(artifacts, "@openai/codex-linux-x64");
|
||||
assert.equal(inspectBundledNativeArtifact(artifact, codex, "linux-x64", data).target, "linux-x64");
|
||||
writeFileSync(join(artifact, "package.json"), JSON.stringify({ name: "@openai/codex-linux-x64", version: "0.160.0-linux-x64", os: ["darwin"], cpu: ["x64"] }));
|
||||
assert.throws(() => inspectBundledNativeArtifact(artifact, codex, "linux-x64", data), /identity mismatch/);
|
||||
});
|
||||
|
||||
test("Linux-produced esbuild bundles publish exact consumer platform dependencies and retain their hooks", (t) => {
|
||||
const { directory, graph, esbuild, producerBinary, publish, writePackage } = esbuildFixture(t);
|
||||
const original = readFileSync(join(esbuild, "package.json"), "utf8");
|
||||
const issuer = createRequire(join(esbuild, "package.json"));
|
||||
// The original producer-only topology cannot resolve a Mac binary. The
|
||||
// public optional declaration lets npm obtain it before offline hooks run.
|
||||
assert.throws(() => issuer.resolve("@esbuild/darwin-arm64/bin/esbuild"), /Cannot find module/);
|
||||
const staged = stageBundledEsbuildOptionalDependencies(directory, publish);
|
||||
for (const target of [...nativeTargets, "win32-x64"]) assert.equal(staged.optionalDependencies[`@esbuild/${target}`], "0.28.2");
|
||||
assert.equal(staged.optionalDependencies.unrelated, "1.0.0");
|
||||
assert.deepEqual(staged.bundleDependencies, ["acpx"]);
|
||||
assert.deepEqual(staged.dependencies, publish.dependencies);
|
||||
assert.equal(publish.optionalDependencies["@esbuild/darwin-arm64"], undefined);
|
||||
assert.equal(existsSync(producerBinary), false);
|
||||
assert.equal(existsSync(join(graph, "unrelated-linux-x64")), true);
|
||||
assert.equal(readFileSync(join(esbuild, "package.json"), "utf8"), original);
|
||||
assert.deepEqual(stageBundledEsbuildOptionalDependencies(directory, staged), staged);
|
||||
for (const target of ["darwin-arm64", "darwin-x64"]) {
|
||||
const artifact = writePackage(join(graph, `@esbuild/${target}`), {
|
||||
name: `@esbuild/${target}`, version: staged.optionalDependencies[`@esbuild/${target}`], os: ["darwin"], cpu: [target.slice(7)],
|
||||
});
|
||||
mkdirSync(join(artifact, "bin")); writeFileSync(join(artifact, "bin/esbuild"), "consumer-selected fixture; never executed");
|
||||
assert.equal(issuer.resolve(`@esbuild/${target}/bin/esbuild`), join(artifact, "bin/esbuild"));
|
||||
}
|
||||
});
|
||||
|
||||
test("bundled esbuild rejects conflicting published or nested platform versions without removing the producer", (t) => {
|
||||
const { directory, graph, producerBinary, publish, writePackage } = esbuildFixture(t);
|
||||
assert.throws(() => stageBundledEsbuildOptionalDependencies(directory, {
|
||||
...publish, optionalDependencies: { "@esbuild/darwin-arm64": "0.27.0" },
|
||||
}), /platform version conflicts/);
|
||||
assert.equal(existsSync(producerBinary), true);
|
||||
writePackage(join(graph, "esbuild"), { name: "esbuild", version: "0.27.0",
|
||||
optionalDependencies: Object.fromEntries(nativeTargets.map(target => [`@esbuild/${target}`, "0.27.0"])) });
|
||||
assert.throws(() => stageBundledEsbuildOptionalDependencies(directory, publish), /platform version conflicts|installed platform identity mismatch/);
|
||||
assert.equal(existsSync(producerBinary), true);
|
||||
});
|
||||
|
||||
test("bundled esbuild rejects missing targets, altered declarations and incorrect platform identity", (t) => {
|
||||
const { directory, esbuild, producerBinary, publish } = esbuildFixture(t);
|
||||
const path = join(esbuild, "package.json"), metadata = JSON.parse(readFileSync(path, "utf8"));
|
||||
writeFileSync(path, JSON.stringify({ ...metadata, optionalDependencies: { "@esbuild/linux-x64": "0.28.2" } }));
|
||||
assert.throws(() => stageBundledEsbuildOptionalDependencies(directory, publish), /omitted a supported platform/);
|
||||
writeFileSync(path, JSON.stringify({ ...metadata, optionalDependencies: { ...metadata.optionalDependencies, "@esbuild/win32-x64": "^0.28.2" } }));
|
||||
assert.throws(() => stageBundledEsbuildOptionalDependencies(directory, publish), /exact package version/);
|
||||
writeFileSync(path, JSON.stringify(metadata));
|
||||
writeFileSync(join(producerBinary, "package.json"), JSON.stringify({ name: "@esbuild/linux-x64", version: "0.27.0" }));
|
||||
assert.throws(() => stageBundledEsbuildOptionalDependencies(directory, publish), /installed platform identity mismatch/);
|
||||
assert.equal(existsSync(producerBinary), true);
|
||||
});
|
||||
|
||||
test("bundled esbuild cannot follow a platform payload or package namespace outside its producer graph", (t) => {
|
||||
const { directory, graph, producerBinary, publish, writePackage } = esbuildFixture(t);
|
||||
const external = writePackage(join(directory, "outside"), { name: "@esbuild/linux-x64", version: "0.28.2" });
|
||||
rmSync(producerBinary, { recursive: true }); symlinkSync(external, producerBinary, "dir");
|
||||
assert.throws(() => stageBundledEsbuildOptionalDependencies(directory, publish), /escapes its producer graph/);
|
||||
assert.equal(readFileSync(join(external, "package.json"), "utf8").includes("0.28.2"), true);
|
||||
rmSync(join(graph, "@esbuild"), { recursive: true }); symlinkSync(external, join(graph, "@esbuild"), "dir");
|
||||
assert.throws(() => stageBundledEsbuildOptionalDependencies(directory, publish), /escapes its producer graph/);
|
||||
});
|
||||
|
||||
test("Codex runtime artifact data preserves the existing Linux qualification without adding targets", () => {
|
||||
assert.deepEqual(Object.keys(runtimeData).sort(), ["codex", "schema"]);
|
||||
assert.deepEqual(Object.keys(runtimeData.codex.platforms), ["linux-x64"]);
|
||||
assert.equal(runtimeData.codex.platforms["linux-x64"].executableDigest, "sha256:12eb3e81114588aca3b7998f4f19e8997b056aca08e57a7ca7c8a3ec8c652aad");
|
||||
assert.equal(runtimeData.codex.platforms["linux-x64"].environmentVariable, "CODEX_PATH");
|
||||
});
|
||||
|
||||
test("Docker provider graphs use qualified targets and contain only native entry bindings without changing server dependencies", (t) => {
|
||||
assert.equal(dockerBundledProviderTarget("amd64"), "linux-x64");
|
||||
assert.equal(dockerBundledProviderTarget("arm64"), null, "Unqualified native ARM64 must not break legacy images");
|
||||
assert.throws(() => dockerBundledProviderTarget("x64"), /declared supported target/);
|
||||
const missing = structuredClone(runtimeData); delete missing.codex.platforms["linux-x64"];
|
||||
assert.equal(dockerBundledProviderTarget("amd64", missing), null, "The existing Codex qualification contract is required");
|
||||
const fixture = realpathSync(mkdtempSync(join(tmpdir(), "paperclip-docker-provider-graph-")));
|
||||
t.after(() => rmSync(fixture, { recursive: true, force: true }));
|
||||
const server = join(fixture, "server"), staged = join(fixture, "staged"), store = join(fixture, "workspace-store");
|
||||
for (const directory of [server, staged, store]) mkdirSync(directory);
|
||||
mkdirSync(join(server, "node_modules/@agentclientprotocol"), { recursive: true });
|
||||
writeFileSync(join(server, "package.json"), JSON.stringify(serverPackage));
|
||||
for (const name of ["@agentclientprotocol/codex-acp", "zod"]) {
|
||||
const directory = join(staged, "node_modules", name); mkdirSync(directory, { recursive: true });
|
||||
writeFileSync(join(directory, "package.json"), JSON.stringify({ name, version: serverPackage.dependencies[name] ?? "4.4.3" }));
|
||||
const previous = join(store, name); mkdirSync(previous, { recursive: true });
|
||||
writeFileSync(join(previous, "package.json"), JSON.stringify({ name, version: "previous-version" }));
|
||||
symlinkSync(previous, join(server, "node_modules", name));
|
||||
}
|
||||
writeFileSync(join(staged, "package.json"), JSON.stringify({ name: serverPackage.name }));
|
||||
const before = readFileSync(join(server, "node_modules/zod/package.json"), "utf8");
|
||||
mergeBundledProviderGraph(staged, server);
|
||||
assert.equal(readFileSync(join(server, "node_modules/zod/package.json"), "utf8"), before, "Application dependencies retain the existing workspace binding");
|
||||
for (const name of ["@agentclientprotocol/codex-acp"]) {
|
||||
assert.equal(lstatSync(join(server, "node_modules", name)).isSymbolicLink(), true);
|
||||
assert.equal(realpathSync(join(server, "node_modules", name)), join(server, "node_modules/.paperclip-native-providers/node_modules", name));
|
||||
assert.equal(JSON.parse(readFileSync(join(store, name, "package.json"))).version, "previous-version", "Workspace store must remain untouched");
|
||||
}
|
||||
assert.throws(() => mergeBundledProviderGraph(staged, server), /destination already exists/);
|
||||
});
|
||||
|
||||
test("Paperclip Runner pins the qualified ACPX host callbacks", () => {
|
||||
assert.equal(rootPackage.pnpm.patchedDependencies["acpx@0.13.1"], "patches/acpx@0.13.1.patch");
|
||||
assert.equal(
|
||||
@@ -285,11 +516,14 @@ test("bundled package patch selection rejects an unpatched installed version", (
|
||||
});
|
||||
|
||||
test("server package staging applies every bundled runtime patch and preserves the vendored runner", (t) => {
|
||||
const fixtureDir = mkdtempSync(join(tmpdir(), "paperclip-bundled-stage-"));
|
||||
const fixtureDir = realpathSync(mkdtempSync(join(tmpdir(), "paperclip-bundled-stage-")));
|
||||
const sourceDir = join(fixtureDir, "source");
|
||||
const destinationDir = join(fixtureDir, "destination");
|
||||
const binDir = join(fixtureDir, "bin");
|
||||
const callLog = join(fixtureDir, "calls.log");
|
||||
const fixtureSourceRoot = join(fixtureDir, "repo");
|
||||
const fixtureArtifacts = join(fixtureDir, "native-artifacts");
|
||||
const fixtureRuntimeData = structuredClone(runtimeData);
|
||||
mkdirSync(sourceDir);
|
||||
mkdirSync(join(sourceDir, "dist"));
|
||||
writeFileSync(join(sourceDir, "dist", "index.js"), "export {};\n");
|
||||
@@ -300,6 +534,18 @@ test("server package staging applies every bundled runtime patch and preserves t
|
||||
JSON.stringify({ ...serverPackage, files: ["dist"] }),
|
||||
);
|
||||
writeFileSync(callLog, "");
|
||||
mkdirSync(fixtureArtifacts);
|
||||
for (const [agent, profile] of Object.entries(profileData.profiles).filter(([agent]) => agent === "codex")) {
|
||||
for (const target of nativeTargets) nativeFixture(fixtureArtifacts, { agent, ...profile }, target, fixtureRuntimeData);
|
||||
}
|
||||
mkdirSync(join(fixtureSourceRoot, "packages/paperclip-runner/src/drivers/acpx"), { recursive: true });
|
||||
writeFileSync(join(fixtureSourceRoot, "package.json"), JSON.stringify(rootPackage));
|
||||
writeFileSync(join(fixtureSourceRoot, "packages/paperclip-runner/acpx-profiles.json"), JSON.stringify(profileData));
|
||||
writeFileSync(join(fixtureSourceRoot, "packages/paperclip-runner/src/drivers/acpx/qualified-runtime-artifacts.json"), JSON.stringify(fixtureRuntimeData));
|
||||
mkdirSync(join(fixtureSourceRoot, "patches"));
|
||||
for (const patchPath of Object.values(rootPackage.pnpm.patchedDependencies)) {
|
||||
writeFileSync(join(fixtureSourceRoot, patchPath), readFileSync(new URL(`../${patchPath}`, import.meta.url)));
|
||||
}
|
||||
t.after(() => rmSync(fixtureDir, { recursive: true, force: true }));
|
||||
|
||||
const writeExecutable = (name, body) => {
|
||||
@@ -320,9 +566,11 @@ mkdir -p "$destination/node_modules/.pnpm"
|
||||
`#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
printf 'npm %s\\n' "$*" >> "$FAKE_CALL_LOG"
|
||||
[ "$*" = "install --omit=dev --ignore-scripts --no-audit --no-fund" ]
|
||||
node -e 'const fs = require("node:fs"); const pkg = require("./package.json"); if ("devDependencies" in pkg) process.exit(1); for (const [name, version] of Object.entries(pkg.dependencies)) { const dir = "node_modules/" + name; fs.mkdirSync(dir + "/dist", { recursive: true }); fs.writeFileSync(dir + "/package.json", JSON.stringify({ name, version })); }'
|
||||
[ "$*" = "install --omit=dev --ignore-scripts --no-audit --no-fund" ] || [ "$*" = "install --force --omit=dev --ignore-scripts --no-audit --no-fund" ]
|
||||
node -e 'const fs = require("node:fs"); const path=require("node:path"); const pkg = require("./package.json"); if ("devDependencies" in pkg) process.exit(1); if(pkg.name === "paperclip-native-artifact-staging"){for(const name of Object.keys(pkg.dependencies)){fs.cpSync(path.join(process.env.FAKE_NATIVE_ARTIFACTS,name),path.join("node_modules",name),{recursive:true});}process.exit(0);} for (const [name, version] of Object.entries(pkg.dependencies)) { const dir = "node_modules/" + name; fs.mkdirSync(dir + "/dist", { recursive: true }); fs.writeFileSync(dir + "/package.json", JSON.stringify({ name, version })); } for (const [bridge, runtimes] of Object.entries(pkg.overrides ?? {})) { for (const [name, version] of Object.entries(runtimes)) { const dir = "node_modules/" + name; fs.mkdirSync(dir, { recursive: true }); const native = name === "@openai/codex"; const optionalDependencies = native ? Object.fromEntries(["linux-x64","darwin-arm64","darwin-x64"].map(target => [name+"-"+target,"npm:"+name+"@"+version+"-"+target])) : undefined; fs.writeFileSync(dir + "/package.json", JSON.stringify({ name, version, optionalDependencies })); } }'
|
||||
if [ -f package.json ] && node -e 'process.exit(require("./package.json").name === "paperclip-native-artifact-staging" ? 0 : 1)'; then exit 0; fi
|
||||
mkdir -p node_modules/acpx/dist
|
||||
node -e 'const fs=require("node:fs");fs.mkdirSync("node_modules/esbuild",{recursive:true});fs.writeFileSync("node_modules/esbuild/package.json",JSON.stringify({name:"esbuild",version:"0.28.2",scripts:{postinstall:"node install.js"},optionalDependencies:Object.fromEntries(["linux-x64","darwin-arm64","darwin-x64"].map(target=>["@esbuild/"+target,"0.28.2"]))}));fs.mkdirSync("node_modules/@esbuild/linux-x64",{recursive:true});fs.writeFileSync("node_modules/@esbuild/linux-x64/package.json",JSON.stringify({name:"@esbuild/linux-x64",version:"0.28.2",os:["linux"],cpu:["x64"]}));'
|
||||
printf 'unpatched runtime\\n' > node_modules/acpx/dist/runtime.js
|
||||
printf '{"name":"acpx","version":"0.13.1"}\\n' > node_modules/acpx/package.json
|
||||
`,
|
||||
@@ -356,9 +604,11 @@ printf 'patched spawnEnvironment runtime\\n' > "$target/dist/runtime.js"
|
||||
execFileSync(
|
||||
process.execPath,
|
||||
[
|
||||
new URL("./prepare-bundled-package.mjs", import.meta.url).pathname,
|
||||
"--input-type=module", "-e",
|
||||
`import { prepareBundledPackage } from ${JSON.stringify(new URL("./prepare-bundled-package.mjs", import.meta.url).href)}; prepareBundledPackage(process.argv[1], process.argv[2], { sourceRoot: process.argv[3] });`,
|
||||
sourceDir,
|
||||
destinationDir,
|
||||
fixtureSourceRoot,
|
||||
],
|
||||
{
|
||||
env: {
|
||||
@@ -366,6 +616,7 @@ printf 'patched spawnEnvironment runtime\\n' > "$target/dist/runtime.js"
|
||||
PATH: `${binDir}:${process.env.PATH}`,
|
||||
FAKE_CALL_LOG: callLog,
|
||||
FAKE_SOURCE_PACKAGE: join(sourceDir, "package.json"),
|
||||
FAKE_NATIVE_ARTIFACTS: fixtureArtifacts,
|
||||
},
|
||||
stdio: "pipe",
|
||||
},
|
||||
@@ -375,6 +626,11 @@ printf 'patched spawnEnvironment runtime\\n' > "$target/dist/runtime.js"
|
||||
assert.equal(lstatSync(stagedAcpxDir).isDirectory(), true);
|
||||
assert.equal(lstatSync(stagedAcpxDir).isSymbolicLink(), false);
|
||||
assert.equal(existsSync(join(destinationDir, "node_modules/.pnpm")), false);
|
||||
const stagedManifest = JSON.parse(readFileSync(join(destinationDir, "package.json"), "utf8"));
|
||||
assert.equal(stagedManifest.optionalDependencies["@esbuild/darwin-arm64"], "0.28.2");
|
||||
assert.equal(stagedManifest.optionalDependencies["@esbuild/darwin-x64"], "0.28.2");
|
||||
assert.equal(existsSync(join(destinationDir, "node_modules/@esbuild/linux-x64")), false);
|
||||
assert.equal(JSON.parse(readFileSync(join(destinationDir, "node_modules/esbuild/package.json"))).scripts.postinstall, "node install.js");
|
||||
assert.match(
|
||||
readFileSync(join(stagedAcpxDir, "dist/runtime.js"), "utf8"),
|
||||
/spawnEnvironment/,
|
||||
@@ -383,13 +639,14 @@ printf 'patched spawnEnvironment runtime\\n' > "$target/dist/runtime.js"
|
||||
readFileSync(callLog, "utf8"),
|
||||
/patch -p1 --forward -d .*node_modules\/acpx/,
|
||||
);
|
||||
const patchedBundledDependencies = serverPackage.bundleDependencies.filter(name => rootPackage.pnpm.patchedDependencies[`${name}@${serverPackage.dependencies[name]}`]);
|
||||
assert.equal(
|
||||
readFileSync(callLog, "utf8")
|
||||
.split("\n")
|
||||
.filter((line) => line.startsWith("patch ")).length,
|
||||
serverPackage.bundleDependencies.length,
|
||||
patchedBundledDependencies.length,
|
||||
);
|
||||
for (const name of serverPackage.bundleDependencies) {
|
||||
for (const name of patchedBundledDependencies) {
|
||||
const specifier = `${name}@${serverPackage.dependencies[name]}`;
|
||||
const patchPath = rootPackage.pnpm.patchedDependencies[specifier];
|
||||
assert.equal(
|
||||
@@ -401,6 +658,23 @@ printf 'patched spawnEnvironment runtime\\n' > "$target/dist/runtime.js"
|
||||
`${specifier} receives its own full configured patch`,
|
||||
);
|
||||
}
|
||||
|
||||
const imageServer = join(fixtureDir, "image-server");
|
||||
mkdirSync(join(imageServer, "node_modules"), { recursive: true });
|
||||
writeFileSync(join(imageServer, "package.json"), JSON.stringify(serverPackage));
|
||||
writeFileSync(join(imageServer, "node_modules/unrelated-marker"), "keep existing application graph");
|
||||
const imageReceipt = JSON.parse(execFileSync(process.execPath, ["--input-type=module", "-e",
|
||||
`import { materializeDockerProviderGraph } from ${JSON.stringify(new URL("./prepare-bundled-package.mjs", import.meta.url).href)};
|
||||
console.log(JSON.stringify(materializeDockerProviderGraph(process.argv[1], 'amd64', { sourceRoot: process.argv[2] })));`,
|
||||
imageServer, fixtureSourceRoot], { env: { ...process.env, PATH: `${binDir}:${process.env.PATH}`,
|
||||
FAKE_CALL_LOG: callLog, FAKE_NATIVE_ARTIFACTS: fixtureArtifacts }, encoding: "utf8" }).trim().split("\n").at(-1));
|
||||
assert.equal(imageReceipt.materialized, true);
|
||||
assert.equal(imageReceipt.artifacts.artifacts.length, 1, "Image must stage only its existing qualified Linux target");
|
||||
assert.equal(imageReceipt.artifacts.artifacts.every(artifact => artifact.target === "linux-x64"), true);
|
||||
assert.equal(readFileSync(join(imageServer, "node_modules/unrelated-marker"), "utf8"), "keep existing application graph");
|
||||
for (const name of ["@agentclientprotocol/codex-acp"]) {
|
||||
assert.match(realpathSync(join(imageServer, "node_modules", name)), /image-server\/node_modules\/\.paperclip-native-providers\/node_modules/);
|
||||
}
|
||||
});
|
||||
|
||||
test("bundled package dry runs preview without querying published versions", () => {
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { createHash } from "node:crypto";
|
||||
import { chmodSync, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const repoRoot = resolve(fileURLToPath(new URL("..", import.meta.url)));
|
||||
const runtimeDataPath = "packages/paperclip-runner/src/drivers/acpx/qualified-runtime-artifacts.json";
|
||||
const nativeTargets = ["linux-x64", "darwin-arm64", "darwin-x64"];
|
||||
const digest = bytes => `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
||||
|
||||
export function materializePublishManifest(pkg) {
|
||||
const publishConfig = pkg.publishConfig ?? {};
|
||||
@@ -48,6 +54,332 @@ export function createBundledInstallManifest(publishManifest, bundledDependencie
|
||||
return installManifest;
|
||||
}
|
||||
|
||||
// npm consumers cannot inherit the workspace's pnpm overrides or patches.
|
||||
// Materialize the already-qualified Codex bridge closure in the tarball,
|
||||
// with overrides confined to the temporary, scripts-disabled producer graph.
|
||||
export function configureBundledProviderOverrides(installManifest, bundledDependencies, rootPackage, profileData,
|
||||
runtimeData = JSON.parse(readFileSync(resolve(repoRoot, runtimeDataPath), "utf8"))) {
|
||||
const result = structuredClone(installManifest);
|
||||
const selected = [];
|
||||
for (const [agent, serverPackage, runtimePackage] of [
|
||||
["codex", "@agentclientprotocol/codex-acp", "@openai/codex"],
|
||||
]) {
|
||||
if (!bundledDependencies.includes(serverPackage)) continue;
|
||||
const profile = profileData?.profiles?.[agent];
|
||||
if (profileData?.schema !== "paperclip.acpx-profiles.v1" || profile?.agentServerPackage !== serverPackage
|
||||
|| profile.agentRuntimePackage !== runtimePackage || !/^\d+\.\d+\.\d+$/.test(profile.agentServerVersion ?? "")
|
||||
|| !/^\d+\.\d+\.\d+$/.test(profile.agentRuntimeVersion ?? "")
|
||||
|| result.dependencies?.[serverPackage] !== profile.agentServerVersion) {
|
||||
throw new Error(`Bundled ${agent} bridge must match its exact qualified profile`);
|
||||
}
|
||||
const selector = `${serverPackage}@${profile.agentServerVersion}>${runtimePackage}`;
|
||||
if (rootPackage.pnpm?.overrides?.[selector] !== profile.agentRuntimeVersion) {
|
||||
throw new Error(`Bundled ${agent} runtime override must match its qualified profile`);
|
||||
}
|
||||
const npmSelector = `${serverPackage}@${profile.agentServerVersion}`;
|
||||
if (result.overrides?.[npmSelector] !== undefined) {
|
||||
throw new Error(`Bundled ${agent} runtime has a conflicting producer override`);
|
||||
}
|
||||
const overrides = { [runtimePackage]: profile.agentRuntimeVersion };
|
||||
result.overrides = { ...result.overrides, [npmSelector]: overrides };
|
||||
selected.push({ agent, ...profile });
|
||||
}
|
||||
return { installManifest: result, profiles: selected };
|
||||
}
|
||||
|
||||
const inside = (root, candidate) => {
|
||||
const value = relative(root, candidate);
|
||||
return value !== "" && value !== ".." && !value.startsWith(`..${sep}`) && !isAbsolute(value);
|
||||
};
|
||||
|
||||
export function inspectBundledNativeArtifact(directory, profile, target, runtimeData) {
|
||||
if (profile.agent !== "codex" || !nativeTargets.includes(target) || runtimeData?.schema !== "paperclip.acpx-runtime-artifacts.v1") throw new Error("Bundled native artifact has no supported target contract");
|
||||
const rootStat = lstatSync(directory), manifestStat = lstatSync(resolve(directory, "package.json"));
|
||||
if (!rootStat.isDirectory() || rootStat.isSymbolicLink() || !manifestStat.isFile() || manifestStat.nlink !== 1 || manifestStat.size > 256 * 1024) {
|
||||
throw new Error("Bundled native artifact manifest is not a bounded regular file");
|
||||
}
|
||||
const [platform, architecture] = target.split("-");
|
||||
const metadata = JSON.parse(readFileSync(resolve(directory, "package.json"), "utf8"));
|
||||
const name = `${profile.agentRuntimePackage}-${target}`;
|
||||
const version = `${profile.agentRuntimeVersion}-${target}`;
|
||||
if (![name, profile.agentRuntimePackage].includes(metadata.name) || metadata.version !== version
|
||||
|| !Array.isArray(metadata.os) || !metadata.os.includes(platform) || !Array.isArray(metadata.cpu) || !metadata.cpu.includes(architecture)
|
||||
|| Object.keys(metadata.dependencies ?? {}).length || Object.keys(metadata.optionalDependencies ?? {}).length || Object.keys(metadata.scripts ?? {}).length) {
|
||||
throw new Error(`Bundled ${profile.agent} native artifact identity mismatch: ${target}`);
|
||||
}
|
||||
const qualification = runtimeData?.[profile.agent]?.platforms?.[target];
|
||||
if (!qualification && !(profile.agent === "codex" && platform === "darwin")) throw new Error("Bundled native artifact omitted its existing executable qualification");
|
||||
const relativeExecutable = qualification?.relativeExecutable ?? (profile.agent === "codex" && platform === "darwin"
|
||||
? `vendor/${architecture === "arm64" ? "aarch64" : "x86_64"}-apple-darwin/bin/codex` : undefined);
|
||||
if (!relativeExecutable || relativeExecutable.split("/").some(value => !value || value === "..") || isAbsolute(relativeExecutable)) {
|
||||
throw new Error("Bundled native artifact has no recognized executable");
|
||||
}
|
||||
const entries = [];
|
||||
let bytes = 0;
|
||||
const walk = (root, prefix = "") => {
|
||||
if (!lstatSync(root).isDirectory() || lstatSync(root).isSymbolicLink()) throw new Error("Bundled native artifact contains a linked directory");
|
||||
for (const entry of readdirSync(root).sort()) {
|
||||
const path = prefix ? `${prefix}/${entry}` : entry;
|
||||
if (/[\u0000-\u001f\u007f\\]/.test(path)) throw new Error("Bundled native artifact contains an unsafe path");
|
||||
const file = resolve(root, entry), stat = lstatSync(file);
|
||||
if (stat.isDirectory()) walk(file, path);
|
||||
else {
|
||||
if (!stat.isFile() || stat.nlink !== 1 || stat.mode & 0o7000) throw new Error("Bundled native artifact contains links or special files");
|
||||
bytes += stat.size;
|
||||
if (bytes > 1024 * 1024 * 1024 || entries.length >= 4096) throw new Error("Bundled native artifact exceeds size limits");
|
||||
entries.push({ path, size: stat.size, sha256: digest(readFileSync(file)), executable: Boolean(stat.mode & 0o111) });
|
||||
}
|
||||
}
|
||||
};
|
||||
walk(directory);
|
||||
const executable = entries.find(entry => entry.path === relativeExecutable);
|
||||
if (!executable?.executable) throw new Error("Bundled native artifact has no executable payload");
|
||||
const header = readFileSync(resolve(directory, relativeExecutable));
|
||||
const correctArchitecture = platform === "linux"
|
||||
? header.length >= 20 && header.subarray(0, 4).equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46])) && header[4] === 2 && header[5] === 1 && header.readUInt16LE(18) === 62
|
||||
: header.length >= 8 && header.readUInt32LE(0) === 0xfeedfacf && header.readUInt32LE(4) === (architecture === "arm64" ? 0x0100000c : 0x01000007);
|
||||
if (!correctArchitecture) throw new Error(`Bundled native artifact architecture mismatch: ${target}`);
|
||||
if (qualification && (qualification.packageName !== name || qualification.packageVersion !== version
|
||||
|| qualification.runtimePackageName !== profile.agentRuntimePackage || qualification.runtimePackageVersion !== profile.agentRuntimeVersion
|
||||
|| qualification.executableDigest !== executable.sha256)) throw new Error(`Bundled native artifact qualified digest mismatch: ${name}`);
|
||||
return { agent: profile.agent, target, packageName: name, packageVersion: version, size: bytes,
|
||||
executable: relativeExecutable, executableDigest: executable.sha256, qualification: qualification ? "existing executable hash" : "native Codex package/version; ACPX qualification unchanged",
|
||||
closureDigest: digest(JSON.stringify(entries)), entries };
|
||||
}
|
||||
|
||||
export function stageBundledProviderOptionalDependencies(destinationDir, publishManifest, profiles,
|
||||
runtimeData = JSON.parse(readFileSync(resolve(repoRoot, runtimeDataPath), "utf8")), { nativeArtifactsDirectory, targets = nativeTargets } = {}) {
|
||||
if (!Array.isArray(targets) || targets.length === 0 || new Set(targets).size !== targets.length
|
||||
|| targets.some(target => !nativeTargets.includes(target))) throw new Error("Invalid bundled native target selection");
|
||||
const result = structuredClone(publishManifest);
|
||||
const graphRoot = realpathSync(resolve(destinationDir, "node_modules"));
|
||||
const artifacts = [], nativeDependencies = {};
|
||||
for (const profile of profiles) {
|
||||
const bridgeManifestPath = resolve(graphRoot, profile.agentServerPackage, "package.json");
|
||||
const bridge = JSON.parse(readFileSync(bridgeManifestPath, "utf8"));
|
||||
if (bridge.name !== profile.agentServerPackage || bridge.version !== profile.agentServerVersion) {
|
||||
throw new Error(`Bundled ${profile.agent} bridge version mismatch`);
|
||||
}
|
||||
const bridgeRequire = createRequire(bridgeManifestPath);
|
||||
const runtimeManifestPath = realpathSync(bridgeRequire.resolve(`${profile.agentRuntimePackage}/package.json`));
|
||||
if (!inside(graphRoot, runtimeManifestPath)) throw new Error("Bundled Codex runtime escapes its producer graph");
|
||||
const runtime = JSON.parse(readFileSync(runtimeManifestPath, "utf8"));
|
||||
if (runtime.name !== profile.agentRuntimePackage || runtime.version !== profile.agentRuntimeVersion) {
|
||||
throw new Error(`Bundled ${profile.agent} runtime version mismatch`);
|
||||
}
|
||||
const optional = Object.entries(runtime.optionalDependencies ?? {});
|
||||
if (optional.length === 0) throw new Error(`Bundled ${profile.agent} runtime omitted platform artifacts`);
|
||||
const runtimeRequire = createRequire(runtimeManifestPath);
|
||||
for (const [name, specifier] of optional) {
|
||||
const suffix = name.slice(profile.agentRuntimePackage.length + 1);
|
||||
const recognized = profile.agent === "codex" && name.startsWith(`${profile.agentRuntimePackage}-`) && /^(linux|darwin|win32)-(x64|arm64)$/.test(suffix);
|
||||
const version = `${runtime.version}-${suffix}`;
|
||||
const expected = `npm:${runtime.name}@${version}`;
|
||||
if (!recognized || specifier !== expected) throw new Error(`Bundled ${profile.agent} platform artifact is not qualified: ${name}`);
|
||||
if (result.optionalDependencies?.[name] !== undefined && result.optionalDependencies[name] !== expected) {
|
||||
throw new Error(`Bundled ${profile.agent} platform artifact conflicts with published dependency: ${name}`);
|
||||
}
|
||||
if (targets.includes(suffix)) {
|
||||
result.optionalDependencies = { ...result.optionalDependencies, [name]: expected };
|
||||
nativeDependencies[name] = expected;
|
||||
}
|
||||
// Remove only recognized producer-platform payloads before staging the
|
||||
// bounded supported target set. Preserve the original runtime manifest.
|
||||
for (const lookup of runtimeRequire.resolve.paths(name) ?? []) {
|
||||
const candidate = resolve(lookup, name);
|
||||
if (!inside(graphRoot, candidate) || !existsSync(candidate)) continue;
|
||||
if (lstatSync(candidate).isSymbolicLink() || !inside(graphRoot, realpathSync(candidate))) {
|
||||
throw new Error(`Bundled ${profile.agent} platform artifact escapes its producer graph`);
|
||||
}
|
||||
const artifact = JSON.parse(readFileSync(resolve(candidate, "package.json"), "utf8"));
|
||||
if (![name, runtime.name].includes(artifact.name) || artifact.version !== version) {
|
||||
throw new Error(`Bundled ${profile.agent} installed platform artifact version mismatch: ${name}`);
|
||||
}
|
||||
rmSync(candidate, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
if (Object.keys(nativeDependencies).length !== profiles.length * targets.length) throw new Error("Bundled provider omitted a supported native target");
|
||||
const temporary = mkdtempSync(resolve(destinationDir, ".paperclip-native-artifacts-"));
|
||||
try {
|
||||
writeFileSync(resolve(temporary, "package.json"), JSON.stringify({ private: true, name: "paperclip-native-artifact-staging", version: "1.0.0", dependencies: nativeDependencies }));
|
||||
if (!nativeArtifactsDirectory) execFileSync("npm", ["install", "--force", "--omit=dev", "--ignore-scripts", "--no-audit", "--no-fund"],
|
||||
{ cwd: temporary, stdio: "inherit", timeout: 180_000 });
|
||||
for (const profile of profiles) for (const target of targets) {
|
||||
const name = `${profile.agentRuntimePackage}-${target}`;
|
||||
const input = resolve(nativeArtifactsDirectory ?? resolve(temporary, "node_modules"), name);
|
||||
const artifact = inspectBundledNativeArtifact(input, profile, target, runtimeData);
|
||||
const output = resolve(graphRoot, name);
|
||||
if (existsSync(output)) throw new Error("Bundled native artifact destination already exists");
|
||||
cpSync(input, output, { recursive: true, dereference: false });
|
||||
for (const entry of artifact.entries) chmodSync(resolve(output, entry.path), entry.executable ? 0o755 : 0o644);
|
||||
const copied = inspectBundledNativeArtifact(output, profile, target, runtimeData);
|
||||
if (copied.closureDigest !== artifact.closureDigest) throw new Error("Bundled native artifact changed during staging");
|
||||
artifacts.push(copied);
|
||||
result.bundleDependencies = [...new Set([...(result.bundleDependencies ?? result.bundledDependencies ?? []), name])];
|
||||
}
|
||||
const total = artifacts.reduce((size, artifact) => size + artifact.size, 0);
|
||||
if (total > 2 * 1024 * 1024 * 1024) throw new Error("Bundled native provider payload exceeds release size limit");
|
||||
result.paperclipProviderArtifacts = { schema: "paperclip.bundled-provider-artifacts.v1", providerCalls: 0, lifecycleScriptsRun: false,
|
||||
totalBytes: total, artifacts: artifacts.map(({ entries, ...artifact }) => artifact) };
|
||||
} finally { rmSync(temporary, { recursive: true, force: true }); }
|
||||
return result;
|
||||
}
|
||||
|
||||
export function dockerBundledProviderTarget(architecture,
|
||||
runtimeData = JSON.parse(readFileSync(resolve(repoRoot, runtimeDataPath), "utf8"))) {
|
||||
const architectures = { amd64: "x64", arm64: "arm64" };
|
||||
if (!Object.hasOwn(architectures, architecture) || runtimeData?.schema !== "paperclip.acpx-runtime-artifacts.v1") {
|
||||
throw new Error("Docker provider materialization requires a declared supported target architecture and qualification contract");
|
||||
}
|
||||
const target = `linux-${architectures[architecture]}`;
|
||||
// A Docker target selects an already-qualified artifact; it cannot qualify
|
||||
// a new platform. Unqualified native targets retain their normal legacy CLI.
|
||||
return runtimeData.codex?.platforms?.[target] ? target : null;
|
||||
}
|
||||
|
||||
export function mergeBundledProviderGraph(stagedDirectory, serverDirectory) {
|
||||
for (const directory of [stagedDirectory, serverDirectory]) {
|
||||
if (!isAbsolute(directory) || resolve(directory) !== directory || directory === "/" || realpathSync(directory) !== directory) {
|
||||
throw new Error("Docker provider graph requires canonical owned package directories");
|
||||
}
|
||||
}
|
||||
const input = resolve(stagedDirectory, "node_modules"), output = resolve(serverDirectory, "node_modules");
|
||||
if (!existsSync(output)) mkdirSync(output);
|
||||
if (realpathSync(input) !== input || realpathSync(output) !== output) throw new Error("Docker provider node_modules cannot escape its package");
|
||||
const names = ["@agentclientprotocol/codex-acp"];
|
||||
const serverMetadata = JSON.parse(readFileSync(resolve(serverDirectory, "package.json"), "utf8"));
|
||||
for (const name of names) {
|
||||
const source = resolve(input, name), stat = lstatSync(source);
|
||||
const metadata = JSON.parse(readFileSync(resolve(source, "package.json"), "utf8"));
|
||||
if (!stat.isDirectory() || stat.isSymbolicLink() || realpathSync(source) !== source
|
||||
|| metadata.name !== name || metadata.version !== serverMetadata.dependencies?.[name]) {
|
||||
throw new Error("Docker provider bridge identity mismatch");
|
||||
}
|
||||
}
|
||||
// Keep the provider closure nested and physically inside the server's own
|
||||
// authority. Changing unrelated direct server bindings (for example zod)
|
||||
// would change application behavior even when no native agent is running.
|
||||
const owned = resolve(output, ".paperclip-native-providers");
|
||||
if (lstatExists(owned)) throw new Error("Docker provider graph destination already exists");
|
||||
cpSync(stagedDirectory, owned, { recursive: true, dereference: false, verbatimSymlinks: true });
|
||||
chmodSync(owned, 0o755);
|
||||
for (const name of names) {
|
||||
const destination = resolve(output, name), namespace = dirname(destination);
|
||||
if (!existsSync(namespace)) mkdirSync(namespace);
|
||||
if (realpathSync(namespace) !== namespace) throw new Error("Docker provider namespace cannot resolve outside the server");
|
||||
if (lstatExists(destination)) {
|
||||
const stat = lstatSync(destination);
|
||||
// pnpm's store is untouched; remove only its server-local package link.
|
||||
if (!stat.isSymbolicLink()) throw new Error("Docker provider entry binding must be a workspace package link");
|
||||
rmSync(destination);
|
||||
}
|
||||
symlinkSync(relative(namespace, resolve(owned, "node_modules", name)), destination, "dir");
|
||||
if (!inside(output, realpathSync(destination))) throw new Error("Docker provider bridge escaped its server authority");
|
||||
}
|
||||
}
|
||||
|
||||
function lstatExists(path) {
|
||||
try { lstatSync(path); return true; }
|
||||
catch (error) { if (error.code === "ENOENT") return false; throw error; }
|
||||
}
|
||||
|
||||
// A Linux-produced bundle contains esbuild's JavaScript but only the producer's
|
||||
// optional executable. Expose its exact platform declarations to the consumer,
|
||||
// just as embedded-postgres does below, before retaining the original hooks.
|
||||
export function stageBundledEsbuildOptionalDependencies(destinationDir, publishManifest) {
|
||||
const graphRoot = resolve(destinationDir, "node_modules");
|
||||
if (realpathSync(graphRoot) !== graphRoot) throw new Error("Bundled esbuild graph must be a canonical owned directory");
|
||||
const result = structuredClone(publishManifest), pending = [{ directory: graphRoot, depth: 0 }];
|
||||
const optional = {}, remove = new Set();
|
||||
let packageCount = 0;
|
||||
const ownedDirectory = directory => {
|
||||
const stat = lstatSync(directory);
|
||||
if (!stat.isDirectory() || stat.isSymbolicLink() || !inside(graphRoot, realpathSync(directory))) {
|
||||
throw new Error("Bundled esbuild dependency escapes its producer graph");
|
||||
}
|
||||
};
|
||||
const manifestAt = directory => {
|
||||
const file = resolve(directory, "package.json"), stat = lstatSync(file);
|
||||
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size > 256 * 1024) {
|
||||
throw new Error("Bundled esbuild dependency manifest must be a bounded regular file");
|
||||
}
|
||||
return JSON.parse(readFileSync(file, "utf8"));
|
||||
};
|
||||
while (pending.length) {
|
||||
const { directory, depth } = pending.pop();
|
||||
if (depth > 64) throw new Error("Bundled esbuild dependency graph exceeds its depth limit");
|
||||
const packages = [];
|
||||
for (const entry of readdirSync(directory)) {
|
||||
if (entry.startsWith(".")) continue;
|
||||
const candidate = resolve(directory, entry);
|
||||
ownedDirectory(candidate);
|
||||
if (entry.startsWith("@")) {
|
||||
for (const child of readdirSync(candidate)) {
|
||||
const scoped = resolve(candidate, child); ownedDirectory(scoped); packages.push(scoped);
|
||||
}
|
||||
} else packages.push(candidate);
|
||||
}
|
||||
for (const packageDirectory of packages) {
|
||||
if (++packageCount > 20_000) throw new Error("Bundled esbuild dependency graph exceeds its package limit");
|
||||
const metadata = manifestAt(packageDirectory);
|
||||
const nested = resolve(packageDirectory, "node_modules");
|
||||
if (lstatExists(nested)) { ownedDirectory(nested); pending.push({ directory: nested, depth: depth + 1 }); }
|
||||
if (basename(packageDirectory) !== "esbuild") continue;
|
||||
if (metadata.name !== "esbuild" || !/^\d+\.\d+\.\d+$/.test(metadata.version ?? "")) {
|
||||
throw new Error("Bundled esbuild package identity mismatch");
|
||||
}
|
||||
const declarations = metadata.optionalDependencies ?? {};
|
||||
if (nativeTargets.some(target => declarations[`@esbuild/${target}`] !== metadata.version)) {
|
||||
throw new Error("Bundled esbuild omitted a supported platform declaration");
|
||||
}
|
||||
const issuer = createRequire(resolve(packageDirectory, "package.json"));
|
||||
for (const [name, version] of Object.entries(declarations)) {
|
||||
if (!/^@esbuild\/[a-z0-9]+-[a-z0-9]+$/.test(name) || version !== metadata.version) {
|
||||
throw new Error("Bundled esbuild platform declaration must match its exact package version");
|
||||
}
|
||||
if ((optional[name] !== undefined && optional[name] !== version)
|
||||
|| (result.optionalDependencies?.[name] !== undefined && result.optionalDependencies[name] !== version)) {
|
||||
throw new Error(`Bundled esbuild platform version conflicts with published dependency: ${name}`);
|
||||
}
|
||||
optional[name] = version;
|
||||
for (const lookup of issuer.resolve.paths(name) ?? []) {
|
||||
const candidate = resolve(lookup, name);
|
||||
if (!inside(graphRoot, candidate) || !lstatExists(candidate)) continue;
|
||||
ownedDirectory(candidate);
|
||||
const installed = manifestAt(candidate);
|
||||
if (installed.name !== name || installed.version !== version) {
|
||||
throw new Error(`Bundled esbuild installed platform identity mismatch: ${name}`);
|
||||
}
|
||||
remove.add(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const directory of remove) rmSync(directory, { recursive: true, force: true });
|
||||
if (Object.keys(optional).length) result.optionalDependencies = { ...result.optionalDependencies, ...optional };
|
||||
return result;
|
||||
}
|
||||
|
||||
export function materializeDockerProviderGraph(serverDirectory, architecture, { sourceRoot = repoRoot } = {}) {
|
||||
const runtimeData = JSON.parse(readFileSync(resolve(sourceRoot, runtimeDataPath), "utf8"));
|
||||
const target = dockerBundledProviderTarget(architecture, runtimeData);
|
||||
if (!target) return { target: `linux-${architecture === "arm64" ? "arm64" : "x64"}`, materialized: false, reason: "Native providers are not qualified for this target; legacy installation preserved" };
|
||||
const metadata = JSON.parse(readFileSync(resolve(serverDirectory, "package.json"), "utf8"));
|
||||
const names = ["@agentclientprotocol/codex-acp"];
|
||||
const temporary = realpathSync(mkdtempSync(resolve(tmpdir(), "paperclip-docker-provider-graph-")));
|
||||
try {
|
||||
const source = resolve(temporary, "source"), staged = resolve(temporary, "staged"); mkdirSync(source);
|
||||
writeFileSync(resolve(source, "package.json"), JSON.stringify({ name: metadata.name, version: metadata.version, type: "module", files: [],
|
||||
dependencies: Object.fromEntries(names.map(name => [name, metadata.dependencies?.[name]])), bundleDependencies: names }));
|
||||
prepareBundledPackage(source, staged, { sourceRoot, targets: [target] });
|
||||
mergeBundledProviderGraph(staged, serverDirectory);
|
||||
const stagedMetadata = JSON.parse(readFileSync(resolve(staged, "package.json"), "utf8"));
|
||||
return { target, materialized: true, artifacts: stagedMetadata.paperclipProviderArtifacts, providerCalls: 0, lifecycleScriptsRun: false };
|
||||
} finally { rmSync(temporary, { recursive: true, force: true }); }
|
||||
}
|
||||
|
||||
function patchedDependencyPackageName(specifier) {
|
||||
const versionSeparator = specifier.lastIndexOf("@");
|
||||
const packageNameEnd = specifier.startsWith("@") ? specifier.indexOf("/") : 0;
|
||||
@@ -136,7 +468,7 @@ export function applyBundledDependencyPatches(destinationDir, bundledDependencie
|
||||
}
|
||||
}
|
||||
|
||||
export function prepareBundledPackage(sourceDir, destinationDir, { sourceRoot = repoRoot } = {}) {
|
||||
export function prepareBundledPackage(sourceDir, destinationDir, { sourceRoot = repoRoot, targets = nativeTargets } = {}) {
|
||||
const sourcePackagePath = resolve(sourceDir, "package.json");
|
||||
const sourcePackage = JSON.parse(readFileSync(sourcePackagePath, "utf8"));
|
||||
const bundledDependencies = sourcePackage.bundleDependencies ?? sourcePackage.bundledDependencies ?? [];
|
||||
@@ -157,7 +489,13 @@ export function prepareBundledPackage(sourceDir, destinationDir, { sourceRoot =
|
||||
|
||||
const deployedPackagePath = resolve(destinationDir, "package.json");
|
||||
const publishManifest = materializePublishManifest(sourcePackage);
|
||||
const installManifest = createBundledInstallManifest(publishManifest, bundledDependencies);
|
||||
const rootPackage = JSON.parse(readFileSync(resolve(sourceRoot, "package.json"), "utf8"));
|
||||
const profileData = bundledDependencies.includes("@agentclientprotocol/codex-acp")
|
||||
? JSON.parse(readFileSync(resolve(sourceRoot, "packages/paperclip-runner/acpx-profiles.json"), "utf8")) : undefined;
|
||||
const runtimeData = profileData ? JSON.parse(readFileSync(resolve(sourceRoot, runtimeDataPath), "utf8")) : undefined;
|
||||
const { installManifest, profiles } = configureBundledProviderOverrides(
|
||||
createBundledInstallManifest(publishManifest, bundledDependencies), bundledDependencies, rootPackage, profileData, runtimeData,
|
||||
);
|
||||
writeFileSync(deployedPackagePath, `${JSON.stringify(installManifest, null, 2)}\n`);
|
||||
|
||||
execFileSync(
|
||||
@@ -167,6 +505,7 @@ export function prepareBundledPackage(sourceDir, destinationDir, { sourceRoot =
|
||||
);
|
||||
writeFileSync(deployedPackagePath, `${JSON.stringify(publishManifest, null, 2)}\n`);
|
||||
applyBundledDependencyPatches(destinationDir, bundledDependencies, sourceRoot);
|
||||
if (profiles.length) writeFileSync(deployedPackagePath, `${JSON.stringify(stageBundledProviderOptionalDependencies(destinationDir, publishManifest, profiles, runtimeData, { targets }), null, 2)}\n`);
|
||||
|
||||
if (bundledDependencies.includes("acpx")) {
|
||||
const acpxPackage = JSON.parse(
|
||||
@@ -210,9 +549,17 @@ export function prepareBundledPackage(sourceDir, destinationDir, { sourceRoot =
|
||||
writeFileSync(deployedPackagePath, `${JSON.stringify(stagedPackage, null, 2)}\n`);
|
||||
rmSync(resolve(destinationDir, "node_modules/@embedded-postgres"), { recursive: true, force: true });
|
||||
}
|
||||
writeFileSync(deployedPackagePath, `${JSON.stringify(stageBundledEsbuildOptionalDependencies(destinationDir,
|
||||
JSON.parse(readFileSync(deployedPackagePath, "utf8"))), null, 2)}\n`);
|
||||
}
|
||||
|
||||
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||
if (process.argv[2] === "--docker-provider-graph") {
|
||||
const [, serverDirectory, architecture, ...extra] = process.argv.slice(2);
|
||||
if (!serverDirectory || !architecture || extra.length) throw new Error("Usage: prepare-bundled-package.mjs --docker-provider-graph <server-directory> <TARGETARCH>");
|
||||
console.log(JSON.stringify(materializeDockerProviderGraph(resolve(serverDirectory), architecture)));
|
||||
process.exit(0);
|
||||
}
|
||||
const [sourceDir, destinationDir] = process.argv.slice(2);
|
||||
if (!sourceDir || !destinationDir) {
|
||||
console.error("Usage: prepare-bundled-package.mjs <source-dir> <destination-dir>");
|
||||
|
||||
Reference in new issue
Block a user