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"),