mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 09:41:04 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - The Claude local adapter can run agent turns through an ACP (Agent Client Protocol) server, `claude-agent-acp`, instead of the plain CLI > - Two separate packages each pin their own copy of that dependency: `packages/adapters/claude-local` (the server-side adapter) and `packages/paperclip-runner` (which builds the provider pack baked into every managed sandbox image) > - `claude-local` moved to `^0.73.0` in #12730, but `paperclip-runner` was never bumped past `0.70.0` — nothing keeps the two in sync when only one changes > - That split means a sandbox image built from `paperclip-runner`'s provider pack ships a `claude-agent-acp` the server-side adapter was never actually compatible with > - This pull request bumps `paperclip-runner`'s pin to `0.73.0`, the only version that satisfies both packages' declared ranges at once, and fixes the matching hardcoded version assertion in `docker/daytona-runner/Dockerfile` > - The benefit is one consistent, compatible `claude-agent-acp` version across both the server host and every sandbox image built from this source, instead of a silent split that only surfaces as a runtime failure ## Linked Issues or Issue Description No public issue exists for this specific split; opening directly per CONTRIBUTING.md path B, following the bug report template fields. **What happened?** `packages/paperclip-runner/package.json` pins `@agentclientprotocol/claude-agent-acp` at an exact `0.70.0`. `packages/adapters/claude-local/package.json` requires `^0.73.0` (added in #12730, 2026-09-02). Nobody re-synced `paperclip-runner`'s pin after that change — the two packages' dependency graphs are independent, so a bump in one doesn't propagate to the other. `paperclip-runner`'s copy is what the fleet sandbox image's provider pack actually ships, so every managed sandbox built from current source carries a `claude-agent-acp` version the server-side adapter's own declared compatibility range excludes. **Expected behavior** The two packages' `claude-agent-acp` pins should stay within a mutually compatible range, so a sandbox image built from this source always ships a version the server-side adapter actually supports. **Steps to reproduce** 1. Check `packages/adapters/claude-local/package.json`'s `@agentclientprotocol/claude-agent-acp` range (`^0.73.0`). 2. Check `packages/paperclip-runner/package.json`'s pin for the same package (`0.70.0` before this PR). 3. Note that `^0.73.0` on a `0.x` version only admits patch releases (`>=0.73.0 <0.74.0` per semver caret rules), so `0.70.0` falls outside it. **Paperclip version or commit** `master` as of this PR (paperclip-runner still at `0.70.0` prior to this change; claude-local's `^0.73.0` requirement landed in #12730). **Deployment mode** Any deployment that runs `claude_local` agents through the ACP engine against a sandbox image built from `packages/paperclip-runner`'s provider pack (managed cloud sandboxes in particular). Related PRs for context (not duplicates — none of these touch `paperclip-runner`'s pin): - #12730 — introduced the `^0.73.0` requirement in `claude-local` - #11873 — the last time `paperclip-runner`'s pin moved (`0.69.0` → `0.70.0`) - #13105 — separately made an unavailable ACP engine a hard failure instead of a silent CLI fallback, which is what turned this version split into a visible, run-blocking error rather than a quiet downgrade ## What Changed - Bump `@agentclientprotocol/claude-agent-acp` from `0.70.0` to `0.73.0` (exact pin, matching this package's existing pin style for its other agent-CLI dependencies) in `packages/paperclip-runner/package.json`. - Update the corresponding hardcoded version assertion (`test "$(claude-agent-acp --version)" = "0.70.0"`) in `docker/daytona-runner/Dockerfile` to `0.73.0`, so its own build-time check stays accurate instead of failing on the next build for an unrelated reason. - `pnpm-lock.yaml` is intentionally **not** included — `pr-trusted.yml`'s `Validate dependency resolution and regenerate stale lockfile` step already regenerates it for the merge tree and hands it to downstream `--frozen-lockfile` jobs as an artifact, so a manual lockfile commit here would just be stale the moment CI runs. ## Verification - `0.73.0` is a real published version on npm (confirmed via `npm view @agentclientprotocol/claude-agent-acp versions`), and it's the *only* version satisfying claude-local's `^0.73.0` range, so this isn't a guess at compatibility — it's the unique intersection of both packages' declared ranges. - `grep -rn "0\.70\.0" docker/ packages/paperclip-runner/package.json` after this change shows no remaining stale references to the old pin. - I did not run a full local install/test pass against a hand-updated lockfile, since regenerating one locally would conflict with leaving `pnpm-lock.yaml` untouched per the note above; CI's own lockfile-regeneration step is the intended verification path for a manifest-only dependency bump like this one. - Downstream/full verification (does a sandbox image actually built with this pin work end-to-end) is tracked separately in `paperclip-cloud` — an unrelated internal-only repo, so not linked here — where a sibling fix restores the ACP servers to the runtime `PATH` in the fleet sandbox image itself; both fixes are needed together for a working sandbox, but this PR is scoped to the version pin alone. ## Risks - Low risk: single-line dependency version bump plus a matching test-assertion update, no code changes. `0.73.0` is a patch release within claude-local's own already-declared-safe range, so there's no reason to expect it changes behavior tenants depend on. - The main risk is unknown breaking changes between `claude-agent-acp` 0.70.0 and 0.73.0 that aren't caught by the version-string assertion alone (that check only confirms the binary reports the right version, not that its behavior is unchanged). I have not audited that package's own changelog between those versions. - `docker/daytona-runner/Dockerfile` is a parallel/reference image (per its own header comment, meant to stay aligned with the private `paperclip-cloud/fleet-sandbox-image/Dockerfile`, which is out of scope here) — this PR does not touch that other Dockerfile. ## Model Used Claude Sonnet 5 (`claude-sonnet-5`), via Claude Code, with tool use (file edits, shell/git, `gh` CLI, `npm view` for version verification). No extended-thinking mode. Standard Claude Code context window. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass — see Verification: a manifest-only bump with the lockfile intentionally left to CI's own regeneration step; no local test run applicable - [x] I have added or updated tests where applicable — version-pin bump only, no new behavior to test - [x] I have updated relevant documentation to reflect my changes — none applicable - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green — pending CI run on this PR - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups — pending review - [x] I will address all Greptile and reviewer comments before requesting merge 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
215 lines
7.0 KiB
JavaScript
215 lines
7.0 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { createHash } from "node:crypto";
|
|
import { readFile } from "node:fs/promises";
|
|
import { createRequire } from "node:module";
|
|
import { dirname, resolve } from "node:path";
|
|
import test from "node:test";
|
|
|
|
const runnerPackage = JSON.parse(
|
|
await readFile(new URL("../package.json", import.meta.url), "utf8"),
|
|
);
|
|
const rootPackage = JSON.parse(
|
|
await readFile(new URL("../../../package.json", import.meta.url), "utf8"),
|
|
);
|
|
const workspace = await readFile(
|
|
new URL("../../../pnpm-workspace.yaml", import.meta.url),
|
|
"utf8",
|
|
);
|
|
const acpxPatch = await readFile(
|
|
new URL("../../../patches/acpx@0.13.1.patch", import.meta.url),
|
|
"utf8",
|
|
);
|
|
const codexPatch = await readFile(
|
|
new URL(
|
|
"../../../patches/@agentclientprotocol__codex-acp@1.6.2.patch",
|
|
import.meta.url,
|
|
),
|
|
"utf8",
|
|
);
|
|
const claudePatch = await readFile(
|
|
new URL(
|
|
"../../../patches/@agentclientprotocol__claude-agent-acp@0.73.0.patch",
|
|
import.meta.url,
|
|
),
|
|
"utf8",
|
|
);
|
|
const qualifiedProfiles = await readFile(
|
|
new URL("../src/drivers/acpx/qualified-profiles.ts", import.meta.url),
|
|
"utf8",
|
|
);
|
|
const runnerdAcpxBackend = await readFile(
|
|
new URL(
|
|
"../runner/crates/runner-core/src/acpx_provider_backend.rs",
|
|
import.meta.url,
|
|
),
|
|
"utf8",
|
|
);
|
|
const providerPackBuilder = await readFile(
|
|
new URL("../scripts/build-provider-pack.mjs", import.meta.url),
|
|
"utf8",
|
|
);
|
|
const nativeSessionExecutor = await readFile(
|
|
new URL(
|
|
"../../../server/src/services/native-runtime/native-session-executor.ts",
|
|
import.meta.url,
|
|
),
|
|
"utf8",
|
|
);
|
|
|
|
test("the runner pins every qualified ACPX production dependency", () => {
|
|
assert.equal(runnerPackage.dependencies["@openai/codex"], "0.153.4");
|
|
assert.equal(runnerPackage.dependencies["@anthropic-ai/claude-agent-sdk"], undefined);
|
|
assert.equal(rootPackage.pnpm.overrides["@agentclientprotocol/codex-acp@1.6.2>@openai/codex"], runnerPackage.dependencies["@openai/codex"]);
|
|
assert.equal(rootPackage.pnpm.overrides["@agentclientprotocol/claude-agent-acp@0.73.0>@anthropic-ai/claude-agent-sdk"], "0.3.263");
|
|
assert.equal(runnerPackage.optionalDependencies, undefined);
|
|
assert.equal(runnerPackage.dependencies.node, undefined);
|
|
assert.equal(runnerPackage.dependencies.acpx, "0.13.1");
|
|
assert.equal(
|
|
runnerPackage.dependencies["@agentclientprotocol/codex-acp"],
|
|
"1.6.2",
|
|
);
|
|
assert.equal(
|
|
runnerPackage.dependencies["@agentclientprotocol/claude-agent-acp"],
|
|
"0.73.0",
|
|
);
|
|
});
|
|
|
|
test("the patched Codex ACP executable digest stays aligned across launch boundaries", async () => {
|
|
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];
|
|
const packagePath = createRequire(import.meta.url).resolve("@agentclientprotocol/codex-acp/package.json");
|
|
const installed = JSON.parse(await readFile(packagePath, "utf8"));
|
|
const executable = await readFile(resolve(dirname(packagePath), installed.bin["codex-acp"]));
|
|
assert.equal(digest, `sha256:${createHash("sha256").update(executable).digest("hex")}`,
|
|
"the identity binds installed executable bytes, not the patch file");
|
|
|
|
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}`,
|
|
),
|
|
);
|
|
});
|
|
|
|
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-acpx-sidecar": "./dist/cli/acpx-runtime-sidecar.js",
|
|
"paperclip-runner-opencode-proxy":
|
|
"./dist/cli/opencode-app-server-proxy.js",
|
|
});
|
|
});
|
|
|
|
test("old and new pnpm configuration both apply the exact runtime patches", () => {
|
|
assert.equal(
|
|
rootPackage.pnpm.patchedDependencies["acpx@0.13.1"],
|
|
"patches/acpx@0.13.1.patch",
|
|
);
|
|
assert.equal(
|
|
rootPackage.pnpm.patchedDependencies[
|
|
"@agentclientprotocol/claude-agent-acp@0.73.0"
|
|
],
|
|
"patches/@agentclientprotocol__claude-agent-acp@0.73.0.patch",
|
|
);
|
|
assert.equal(
|
|
rootPackage.pnpm.patchedDependencies[
|
|
"@agentclientprotocol/codex-acp@1.6.2"
|
|
],
|
|
"patches/@agentclientprotocol__codex-acp@1.6.2.patch",
|
|
);
|
|
assert.match(workspace, /acpx@0\.13\.1: patches\/acpx@0\.13\.1\.patch/);
|
|
assert.match(
|
|
workspace,
|
|
/codex-acp@1\.6\.2["']: patches\/@agentclientprotocol__codex-acp@1\.6\.2\.patch/,
|
|
);
|
|
assert.match(
|
|
workspace,
|
|
/claude-agent-acp@0\.73\.0["']: patches\/@agentclientprotocol__claude-agent-acp@0\.73\.0\.patch/,
|
|
);
|
|
assert.equal(rootPackage.pnpm.patchedDependencies["node@24.11.0"], undefined);
|
|
assert.doesNotMatch(workspace, /node@24\.11\.0:/);
|
|
assert.match(
|
|
providerPackBuilder,
|
|
/copyFileSync\(process\.execPath, stableNodeCommand\)/,
|
|
);
|
|
assert.match(codexPatch, /\+ "@openai\/codex": "0\.153\.4"/);
|
|
});
|
|
|
|
test("the ACPX patch preserves launch-only state and verified spawning", () => {
|
|
for (const token of [
|
|
"spawnEnvironment",
|
|
"spawnCwd",
|
|
"spawnAgent",
|
|
"SpawnOptionsWithoutStdio",
|
|
"this.options.spawnAgent",
|
|
]) {
|
|
assert.match(acpxPatch, new RegExp(token));
|
|
}
|
|
});
|
|
|
|
test("the ACPX patch fails closed on an invalid spawn environment", () => {
|
|
for (const token of [
|
|
"isPlainStringEnvironment",
|
|
"Object.getPrototypeOf(value)",
|
|
'Object.values(value).every((entry) => typeof entry === "string")',
|
|
"spawnEnvironment !== void 0",
|
|
"sourceEnvironment = spawnEnvironment()",
|
|
"ACPX spawn environment must be a plain record of string values",
|
|
]) {
|
|
assert.match(
|
|
acpxPatch,
|
|
new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")),
|
|
);
|
|
}
|
|
assert.doesNotMatch(acpxPatch, /spawnEnvironment\?\.\(\)/);
|
|
assert.doesNotMatch(
|
|
acpxPatch,
|
|
/spawnEnvironment \? \{ \.\.\.spawnEnvironment \} : \{ \.\.\.process\.env \}/,
|
|
);
|
|
});
|
|
|
|
test("the Codex patch enforces isolated instructions, tools, and skills", () => {
|
|
for (const token of [
|
|
"PAPERCLIP_ACPX_ISOLATED_CONTEXT",
|
|
"baseInstructions",
|
|
"rawInput: { serverName: params.serverName }",
|
|
'"features.apps": false',
|
|
"process.env.CODEX_HOME",
|
|
]) {
|
|
assert.match(
|
|
codexPatch,
|
|
new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")),
|
|
);
|
|
}
|
|
});
|
|
|
|
test("the Codex patch keeps MCP tool approvals on the governed permission channel", () => {
|
|
assert.match(
|
|
codexPatch,
|
|
/!context\.isToolApproval && this\.shouldUseAcpElicitation\(params\)/,
|
|
);
|
|
});
|
|
|
|
test("the Claude patch removes ambient project and local configuration", () => {
|
|
for (const token of [
|
|
"PAPERCLIP_ACPX_ISOLATED_CONTEXT",
|
|
'settingSources: ["user"]',
|
|
"userProvidedOptions?.mcpServers",
|
|
]) {
|
|
assert.match(
|
|
claudePatch,
|
|
new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")),
|
|
);
|
|
}
|
|
});
|