fix(runner): remove provider pack install hook

This commit is contained in:
Dotta 2026-09-03 04:39:09 -05:00
parent 43558e7783
commit fb40d8a307
6 changed files with 61 additions and 63 deletions

View File

@ -103,8 +103,7 @@
"acpx@0.13.1": "patches/acpx@0.13.1.patch",
"@agentclientprotocol/claude-agent-acp@0.70.0": "patches/@agentclientprotocol__claude-agent-acp@0.70.0.patch",
"@agentclientprotocol/claude-agent-acp@0.73.0": "patches/@agentclientprotocol__claude-agent-acp@0.73.0.patch",
"@agentclientprotocol/codex-acp@1.6.2": "patches/@agentclientprotocol__codex-acp@1.6.2.patch",
"node@24.11.0": "patches/node@24.11.0.patch"
"@agentclientprotocol/codex-acp@1.6.2": "patches/@agentclientprotocol__codex-acp@1.6.2.patch"
},
"overrides": {
"rollup": ">=4.59.0",

View File

@ -158,13 +158,13 @@
"acpx": "0.13.1",
"ajv": "^8.20.0",
"json-schema-to-ts": "^3.1.1",
"node": "24.11.0",
"opencode-ai": "1.18.17",
"react-markdown": "^10.1.0",
"remark-gfm": "^4.0.1"
},
"optionalDependencies": {
"@openai/codex-linux-x64": "npm:@openai/codex@0.148.0-linux-x64"
"@openai/codex-linux-x64": "npm:@openai/codex@0.148.0-linux-x64",
"node-linux-x64": "24.11.0"
},
"peerDependencies": {
"react": ">=18",

View File

@ -24,16 +24,14 @@ const outputRoot = resolve(
outputArgument ?? join(packageRoot, "provider-pack"),
);
if (
outputRoot === workspaceRoot
|| outputRoot === packageRoot
|| outputRoot === "/"
outputRoot === workspaceRoot ||
outputRoot === packageRoot ||
outputRoot === "/"
) {
throw new Error(`Refusing unsafe provider-pack output path: ${outputRoot}`);
}
const temporaryParent = mkdtempSync(
join(tmpdir(), "paperclip-provider-pack-"),
);
const temporaryParent = mkdtempSync(join(tmpdir(), "paperclip-provider-pack-"));
const temporaryRoot = join(temporaryParent, "pack");
function canonicalJson(value) {
@ -56,8 +54,9 @@ function sha256File(path) {
function sha256Tree(root) {
const hash = createHash("sha256");
const visit = (directory, prefix = "") => {
const entries = readdirSync(directory, { withFileTypes: true })
.sort((left, right) => left.name.localeCompare(right.name));
const entries = readdirSync(directory, { withFileTypes: true }).sort(
(left, right) => left.name.localeCompare(right.name),
);
for (const entry of entries) {
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
const absolutePath = join(directory, entry.name);
@ -67,9 +66,13 @@ function sha256Tree(root) {
} else if (entry.isFile()) {
hash.update(`file\0${relativePath}\0${sha256File(absolutePath)}\n`);
} else if (entry.isSymbolicLink()) {
hash.update(`symlink\0${relativePath}\0${readlinkSync(absolutePath)}\n`);
hash.update(
`symlink\0${relativePath}\0${readlinkSync(absolutePath)}\n`,
);
} else {
throw new Error(`Provider pack tree contains unsupported entry ${relativePath}`);
throw new Error(
`Provider pack tree contains unsupported entry ${relativePath}`,
);
}
}
};
@ -123,6 +126,22 @@ try {
throw new Error(`pnpm deploy failed with exit code ${deployed.status}`);
}
// The generic `node` npm package runs an install-time downloader. Depend on
// the immutable Linux x64 artifact directly, then expose it at the stable
// pack-owned path consumed by the verified launch contract.
const packagedNodeRoot = join(
temporaryRoot,
"node_modules",
"node-linux-x64",
);
const stableNodeRoot = join(temporaryRoot, "node_modules", "node");
if (!existsSync(packagedNodeRoot) || existsSync(stableNodeRoot)) {
throw new Error(
"Provider pack requires the pinned node-linux-x64 artifact and an unclaimed stable Node path",
);
}
renameSync(packagedNodeRoot, stableNodeRoot);
// pnpm's generated .bin shims embed the temporary deployment directory in
// NODE_PATH. That makes an otherwise identical provider pack hash differ on
// every build and leaks a nonexistent host path after relocation. Replace
@ -193,7 +212,10 @@ try {
const opencodeExecutable = "node_modules/opencode-ai/bin/opencode.exe";
const nodeCommand = "node_modules/node/bin/node";
const productionLock = "pnpm-lock.yaml";
copyFileSync(join(workspaceRoot, "pnpm-lock.yaml"), join(temporaryRoot, productionLock));
copyFileSync(
join(workspaceRoot, "pnpm-lock.yaml"),
join(temporaryRoot, productionLock),
);
for (const relativePath of [
nodeCommand,
productionLock,
@ -207,16 +229,14 @@ try {
}
}
const opencodeProxySha = sha256File(
join(temporaryRoot, opencodeProxyPath),
);
const opencodeProxySha = sha256File(join(temporaryRoot, opencodeProxyPath));
const acpxSidecarSha = sha256File(join(temporaryRoot, acpxSidecarPath));
const distDigest = sha256Tree(join(temporaryRoot, "dist"));
const configuredRevision =
process.env.PAPERCLIP_RUNNER_SOURCE_REVISION?.trim();
const revision =
configuredRevision
?? execFileSync("git", ["rev-parse", "HEAD"], {
configuredRevision ??
execFileSync("git", ["rev-parse", "HEAD"], {
cwd: workspaceRoot,
encoding: "utf8",
}).trim();
@ -225,11 +245,9 @@ try {
}
const dirty = configuredRevision
? false
: spawnSync(
"git",
["diff", "--quiet", "--", "packages/paperclip-runner"],
{ cwd: workspaceRoot },
).status !== 0;
: spawnSync("git", ["diff", "--quiet", "--", "packages/paperclip-runner"], {
cwd: workspaceRoot,
}).status !== 0;
const payload = {
pins: {
nodeMinimum: "24.11.0",

View File

@ -30,10 +30,6 @@ const claudePatch = await readFile(
),
"utf8",
);
const nodePatch = await readFile(
new URL("../../../patches/node@24.11.0.patch", import.meta.url),
"utf8",
);
const qualifiedProfiles = await readFile(
new URL("../src/drivers/acpx/qualified-profiles.ts", import.meta.url),
"utf8",
@ -58,12 +54,13 @@ const nativeSessionExecutor = await readFile(
);
test("the runner pins every qualified ACPX production dependency", () => {
assert.equal(runnerPackage.dependencies.node, "24.11.0");
assert.equal(runnerPackage.dependencies["@openai/codex"], "0.148.0");
assert.equal(
runnerPackage.optionalDependencies["@openai/codex-linux-x64"],
"npm:@openai/codex@0.148.0-linux-x64",
);
assert.equal(runnerPackage.optionalDependencies["node-linux-x64"], "24.11.0");
assert.equal(runnerPackage.dependencies.node, undefined);
assert.equal(runnerPackage.dependencies.acpx, "0.13.1");
assert.equal(
runnerPackage.dependencies["@agentclientprotocol/codex-acp"],
@ -76,31 +73,30 @@ test("the runner pins every qualified ACPX production dependency", () => {
});
test("the patched Codex ACP command digest stays aligned across launch boundaries", () => {
const profileMatch = /agent: "codex"[\s\S]*?commandDigest:\s*"(sha256:[a-f0-9]{64})"/.exec(
qualifiedProfiles,
);
const profileMatch =
/agent: "codex"[\s\S]*?commandDigest:\s*"(sha256:[a-f0-9]{64})"/.exec(
qualifiedProfiles,
);
assert.ok(profileMatch, "qualified Codex ACPX profile digest");
const digest = profileMatch[1];
assert.match(
runnerdAcpxBackend,
new RegExp(`"codex"[\\s\\S]*?${digest}`),
);
assert.match(runnerdAcpxBackend, new RegExp(`"codex"[\\s\\S]*?${digest}`));
assert.match(
providerPackBuilder,
new RegExp(`acpxProfileDigests:[\\s\\S]*?codex:[\\s\\S]*?${digest}`),
);
assert.match(
nativeSessionExecutor,
new RegExp(`REMOTE_PROVIDER_PACK_PROFILE_DIGESTS[\\s\\S]*?codex:[\\s\\S]*?${digest}`),
new RegExp(
`REMOTE_PROVIDER_PACK_PROFILE_DIGESTS[\\s\\S]*?codex:[\\s\\S]*?${digest}`,
),
);
});
test("the package exposes only the reviewed runner CLI binaries", () => {
assert.deepEqual(runnerPackage.bin, {
"paperclip-runner-eval-session": "./dist/cli/eval-session.js",
"paperclip-runner-codex-proxy":
"./dist/cli/codex-app-server-unix-proxy.js",
"paperclip-runner-codex-proxy": "./dist/cli/codex-app-server-unix-proxy.js",
"paperclip-runner-acpx-sidecar": "./dist/cli/acpx-runtime-sidecar.js",
"paperclip-runner-opencode-proxy":
"./dist/cli/opencode-app-server-proxy.js",
@ -133,13 +129,12 @@ test("old and new pnpm configuration both apply the exact runtime patches", () =
workspace,
/claude-agent-acp@0\.70\.0': patches\/@agentclientprotocol__claude-agent-acp@0\.70\.0\.patch/,
);
assert.equal(
rootPackage.pnpm.patchedDependencies["node@24.11.0"],
"patches/node@24.11.0.patch",
assert.equal(rootPackage.pnpm.patchedDependencies["node@24.11.0"], undefined);
assert.doesNotMatch(workspace, /node@24\.11\.0:/);
assert.match(
providerPackBuilder,
/renameSync\(packagedNodeRoot, stableNodeRoot\)/,
);
assert.match(workspace, /node@24\.11\.0: patches\/node@24\.11\.0\.patch/);
assert.match(nodePatch, /- "bin": \{/);
assert.match(nodePatch, /- "node": "bin\/node"/);
});
test("the ACPX patch preserves launch-only state and verified spawning", () => {

View File

@ -1,13 +0,0 @@
diff --git a/package.json b/package.json
--- a/package.json
+++ b/package.json
@@ -14,9 +14,6 @@
"scripts": {
"preinstall": "node installArchSpecificPackage"
},
- "bin": {
- "node": "bin/node"
- },
"dependencies": {
"node-bin-setup": "^1.0.0"
},

View File

@ -19,7 +19,6 @@ patchedDependencies:
embedded-postgres@18.1.0-beta.16: patches/embedded-postgres@18.1.0-beta.16.patch
acpx@0.12.0: patches/acpx@0.12.0.patch
acpx@0.13.1: patches/acpx@0.13.1.patch
'@agentclientprotocol/claude-agent-acp@0.70.0': patches/@agentclientprotocol__claude-agent-acp@0.70.0.patch
'@agentclientprotocol/claude-agent-acp@0.73.0': patches/@agentclientprotocol__claude-agent-acp@0.73.0.patch
'@agentclientprotocol/codex-acp@1.6.2': patches/@agentclientprotocol__codex-acp@1.6.2.patch
node@24.11.0: patches/node@24.11.0.patch
"@agentclientprotocol/claude-agent-acp@0.70.0": patches/@agentclientprotocol__claude-agent-acp@0.70.0.patch
"@agentclientprotocol/claude-agent-acp@0.73.0": patches/@agentclientprotocol__claude-agent-acp@0.73.0.patch
"@agentclientprotocol/codex-acp@1.6.2": patches/@agentclientprotocol__codex-acp@1.6.2.patch