From 036b1860c374f54b4360657b13eadb27ae78e18d Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Thu, 5 Nov 2026 11:04:13 -0600 Subject: [PATCH] fix(runner): bind Cursor sidecars to the runner package, not the workspace root (#19255) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - The native runner starts ACPX providers through a sidecar. runnerd tells the sidecar which package owns the provider assets. > - Cursor, Copilot, Pi, Kimi and Antigravity use pinned native closures. Their asset guard requires the bound manifest to be the `package.json` directly inside the bound root. > - In a local source build, runnerd bound Cursor to the pnpm workspace root, with the runner manifest one level down. The guard threw before it checked the Cursor closure. > - Every Cursor run on a local source instance failed with `admission_installation`. The sidecar redacts the message, so the cause was not visible. > - This pull request gives Cursor the same runner-package authority as the other native-asset providers. > - The benefit is that Cursor runs start on local source builds. ## Linked Issues or Issue Description **What happened?** A `paperclip_runner` agent with `provider: "acpx"` and `acpxAgent: "cursor"` failed at every start on a local macOS worktree instance. The run log showed `ACPX sidecar command session.open was rejected (retryable=false, classification=admission_installation)`. **Expected behavior** The run starts, because the pinned Cursor runtime cache matches `cursor-distributions.json`. **Steps to reproduce** 1. Run `pnpm dev` from a source checkout, with the Cursor runtime set up in `~/.paperclip/runtimes/cursor`. 2. Create a native runner agent with `acpxAgent: "cursor"` and a bound `CURSOR_AUTH_TOKEN`. 3. Start a run. It fails at `session.open`. **Paperclip version or commit** `master` at 26391707fb. **Agent adapter(s) involved** Cursor (native runner, ACPX). **Relevant logs or output** `failed to start ACPX provider: ACPX sidecar command session.open was rejected (retryable=false, classification=admission_installation)` **Additional context** `acpxProviderPackageAuthority` sent Cursor through the JS-provider branch. That branch binds `PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT` to the workspace dependency root. `resolveRunnerProviderAssetsRoot` then threw `Runner provider manifest is outside its package root`. With the same environment, a direct call to the built verifier reproduces that error. ## What Changed - Add one `NATIVE_ASSET_ACPX_AGENTS` set (`cursor`, `copilot`, `pi`, `kimi`, `antigravity`) in `runnerd-codex-transport.ts`. - The runner-package branch uses the set. This adds Cursor. - The vendored source-server branch uses the same set. This adds Cursor, Copilot and Kimi, which had the same gap there. - Tests cover Cursor and Kimi in the source, scoped-npm and deployment layouts. Tests also cover all five providers in the vendored source-server layout. ## Verification - `npx vitest run src/live/runnerd-codex-transport.test.ts -t "package authority|vendored source server"` in `packages/paperclip-runner`: 18 pass. - Red check: on the old code, four new Cursor cases fail. - Full `runnerd-codex-transport.test.ts`: the same 56 tests fail before and after this change. They fail because the fresh worktree has no built `runner/target/debug/paperclip-runnerd`. The change adds no new failures. - Live: on a worktree instance with this fix, a Cursor run (`gpt-5.6-luna`) started and succeeded. It sent five `plan.updated` events, and the task chat showed the plan card. - Manual: I drove the built `acpx-runtime-sidecar.cjs` with `initialize` and `session.open` for Cursor. With the runner-package binding, installation admission passes. With the workspace-root binding, the verifier throws the manifest error. ## Risks - Low risk. The change only selects the package root for providers whose asset resolver already rejected the workspace root. Those providers never read JS dependencies from the bound root. - Published npm server bundles and provider packs are unchanged. Their sidecar is not inside the owner package, or they already bind the server package. ## Model Used - Claude Opus 5.5 (`claude-opus-5-5`) in Claude Code, with tool use and code execution. ## 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 - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [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 Opus 5.5 --- .../src/drivers/acpx/provider-assets-root.ts | 6 ++++-- .../src/live/runnerd-codex-transport.test.ts | 9 +++++++-- .../src/live/runnerd-codex-transport.ts | 11 +++++++++-- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/packages/paperclip-runner/src/drivers/acpx/provider-assets-root.ts b/packages/paperclip-runner/src/drivers/acpx/provider-assets-root.ts index 78ef412f092..1161d69800a 100644 --- a/packages/paperclip-runner/src/drivers/acpx/provider-assets-root.ts +++ b/packages/paperclip-runner/src/drivers/acpx/provider-assets-root.ts @@ -2,7 +2,9 @@ import { constants, closeSync, fstatSync, lstatSync, openSync, readFileSync, rea import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path"; import { fileURLToPath } from "node:url"; -type NativeProvider = "cursor" | "copilot" | "pi" | "kimi" | "antigravity"; +/** Single source of truth for providers whose pinned native closures resolve here. */ +export const NATIVE_ASSET_PROVIDERS = ["cursor", "copilot", "pi", "kimi", "antigravity"] as const; +export type NativeProvider = typeof NATIVE_ASSET_PROVIDERS[number]; const RUNNER_PACKAGE_NAME = "@paperclipai/paperclip-runner"; const SERVER_PACKAGE_NAME = "@paperclipai/server"; function assertServerVendorLayout(root: string): void { @@ -17,7 +19,7 @@ function assertServerVendorLayout(root: string): void { /** Resolve only runner-owned package assets, including descriptor-loaded sidecars. */ export function resolveRunnerProviderAssetsRoot(moduleUrl: string, provider: NativeProvider): string { - if (!["cursor", "copilot", "pi", "kimi", "antigravity"].includes(provider)) throw new Error("Unknown native ACP provider assets"); + if (!(NATIVE_ASSET_PROVIDERS as readonly string[]).includes(provider)) throw new Error("Unknown native ACP provider assets"); const boundRoot = process.env.PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT; const boundManifest = process.env.PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST; let packageRoot: string; diff --git a/packages/paperclip-runner/src/live/runnerd-codex-transport.test.ts b/packages/paperclip-runner/src/live/runnerd-codex-transport.test.ts index fdcb5a2cc54..2faa2cb9c82 100644 --- a/packages/paperclip-runner/src/live/runnerd-codex-transport.test.ts +++ b/packages/paperclip-runner/src/live/runnerd-codex-transport.test.ts @@ -2063,17 +2063,22 @@ it.each([false, true])("binds a vendored source server to its pnpm dependency ro expect(runnerdLaunchProfileInternals.acpxProviderPackageAuthority( join(server, "dist/vendor/paperclip-runner/cli/acpx-runtime-sidecar.cjs"), )).toEqual({ root: selfRooted ? server : workspace, manifest: join(server, "package.json") }); - for (const agent of ["pi", "antigravity"] as const) { + for (const agent of ["cursor", "copilot", "pi", "kimi", "antigravity"] as const) { expect(runnerdLaunchProfileInternals.acpxProviderPackageAuthority( join(server, "dist/vendor/paperclip-runner/cli/acpx-runtime-sidecar.cjs"), workspace, agent, )).toEqual({ root: server, manifest: join(server, "package.json") }); } + for (const agent of ["claude", "codex", "grok"] as const) { + expect(runnerdLaunchProfileInternals.acpxProviderPackageAuthority( + join(server, "dist/vendor/paperclip-runner/cli/acpx-runtime-sidecar.cjs"), workspace, agent, + )).toEqual({ root: selfRooted ? server : workspace, manifest: join(server, "package.json") }); + } } finally { await rm(workspace, { recursive: true, force: true }); } }); -it.each((["copilot", "pi", "antigravity"] as const).flatMap(agent => +it.each((["cursor", "copilot", "pi", "kimi", "antigravity"] as const).flatMap(agent => ["source", "scoped-npm", "deployment"].map(layout => ({ agent, layout }))))( "admits the controller-selected $agent package authority in a $layout layout", async ({ agent, layout }) => { diff --git a/packages/paperclip-runner/src/live/runnerd-codex-transport.ts b/packages/paperclip-runner/src/live/runnerd-codex-transport.ts index d81e7a08123..8b81b27ad8f 100644 --- a/packages/paperclip-runner/src/live/runnerd-codex-transport.ts +++ b/packages/paperclip-runner/src/live/runnerd-codex-transport.ts @@ -17,6 +17,7 @@ import { admittedPiThinkingLevel, resolvePiThinkingLevel } from "../drivers/acpx import { configuredEnvironment } from "../configured-environment.js"; import { githubCredentialEnvironment } from "../github-credential-environment.js"; import { resolveAcpxProviderMode } from "../drivers/acpx/provider-mode.js"; +import { NATIVE_ASSET_PROVIDERS } from "../drivers/acpx/provider-assets-root.js"; import { preconfiguredHomeEnvironment, preconfiguredCodexHome, usesPreconfiguredHome } from "../drivers/preconfigured-home.js"; import { isAcpxCanonicalInputMethod } from "../drivers/acpx/profile-extensions.js"; import { RunnerdTraceFrameIndex } from "./runnerd-trace-frame-index.js"; @@ -3070,6 +3071,12 @@ function resolveBuildOwnedCliArtifact( ); } +// Providers whose pinned native closures resolve through +// resolveRunnerProviderAssetsRoot. That guard requires the bound manifest to be +// the package.json directly inside the bound root, so these providers must never +// receive the workspace dependency root used by JS-package providers. +const NATIVE_ASSET_ACPX_AGENTS: ReadonlySet = new Set(NATIVE_ASSET_PROVIDERS); + function acpxProviderPackageAuthority( sidecarScript: string, ownerPackageRoot = packageRoot, @@ -3089,7 +3096,7 @@ function acpxProviderPackageAuthority( const workspaceRoot = dirname(serverRoot); // Source/Docker builds share the workspace's pnpm store. Published npm // bundles and self-rooted pnpm deployments retain their server authority. - if (agent !== "pi" && agent !== "antigravity" && basename(serverRoot) === "server" && + if ((agent === undefined || !NATIVE_ASSET_ACPX_AGENTS.has(agent)) && basename(serverRoot) === "server" && !existsSync(resolve(serverRoot, "node_modules/.pnpm")) && existsSync(resolve(workspaceRoot, "node_modules/.pnpm")) && existsSync(resolve(workspaceRoot, "pnpm-workspace.yaml")) && @@ -3115,7 +3122,7 @@ function acpxProviderPackageAuthority( // Native provider closures belong to the runner package itself. Their asset // guard requires the bound manifest directly inside that same package root; // the workspace dependency root used by JS providers is a different owner. - if (agent === "copilot" || agent === "pi" || agent === "kimi" || agent === "antigravity") { + if (agent !== undefined && NATIVE_ASSET_ACPX_AGENTS.has(agent)) { return { root: sidecarPackageRoot, manifest: resolve(sidecarPackageRoot, "package.json"),