mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-08 22:13:59 +01:00
fix(runner): bind Cursor sidecars to the runner package, not the workspace root (#19255)
## 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 <noreply@anthropic.com>
This commit is contained in:
1 parent
3d0d178ff7
commit
036b1860c3
3 files changed
+20
-6
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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<QualifiedAcpxAgent> = 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"),
|
||||
|
||||
Reference in new issue
Block a user