paperclipai/paperclip · error

ACPX provider node_modules resolves outside the selected…

Error message

ACPX provider node_modules resolves outside the selected provider root

What it means

The resolver canonicalizes <canonicalRoot>/node_modules and requires the result to remain inside the canonical provider root. If node_modules is a symlink to an external directory (e.g. a shared pnpm store), dependency resolution would read packages from outside the vetted provider installation, so the library rejects the layout to keep dependency provenance auditable.

Solutions

  1. Install provider dependencies so node_modules is a real directory inside the provider root (npm/yarn install directly in the provider package, or copy the tree into root)
  2. Use pnpm's node-linker=hoisted in .npmrc for the provider install to avoid symlinked layouts
  3. Point the provider root at the directory whose realpath actually contains node_modules
  4. In containers, mount/copy node_modules inside the provider root rather than linking from outside

Example fix

// before
ln -s /store/acpx-provider/node_modules /opt/acpx/provider/node_modules
// after
# .npmrc in provider root
node-linker=hoisted
$ npm install --prefix /opt/acpx/provider   # real node_modules inside root
Defensive patterns

Strategy: validation

Validate before calling

import fs from 'node:fs';
import path from 'node:path';
function nodeModulesInsideRoot(root: string): boolean {
  const canonicalRoot = fs.realpathSync(root);
  const nm = fs.realpathSync(path.resolve(canonicalRoot, 'node_modules'));
  return nm.startsWith(canonicalRoot + path.sep);
}

Try / catch

try {
  const resolver = createAcpxPackageJsonResolver(root);
} catch (err) {
  if (err instanceof Error && /node_modules resolves outside the selected provider root/.test(err.message)) {
    throw new Error(`Provider ${root} uses a symlinked node_modules store; reinstall with node-linker=hoisted or copy node_modules into the provider root`);
  }
  throw err;
}

Prevention

When it happens

Trigger: Provider root contains node_modules as a symlink pointing outside root, so realpathSync(resolve(canonicalRoot, 'node_modules')) escapes canonicalRoot; ancestor dependency resolution would then walk an unvetted store path.

Common situations: Installing the provider with pnpm (which symlinks node_modules/.pnpm and may link node_modules itself); a monorepo hoisting node_modules to the repo root while root is a subpackage; docker/volume setups that relocate node_modules outside the provider directory.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-10). Data as JSON: /api/errors/ca0489b84f0f685e. Report an issue: GitHub.

Appendix: source

Thrown at packages/paperclip-runner/src/drivers/acpx/installation-integrity.ts:328

    manifest.includes("\0") ||
    resolve(manifest) !== manifest
  ) {
    throw new Error(
      "ACPX provider package manifest must be an explicit normalized absolute path",
    );
  }
  const canonicalRoot = realpathSync(root);
  const canonicalManifest = realpathSync(manifest);
  if (!pathIsInside(canonicalRoot, canonicalManifest)) {
    throw new Error(
      "ACPX provider package manifest resolves outside the selected provider root",
    );
  }
  const canonicalNodeModules = realpathSync(
    resolve(canonicalRoot, "node_modules"),
  );
  if (!pathIsInside(canonicalRoot, canonicalNodeModules)) {
    throw new Error(
      "ACPX provider node_modules resolves outside the selected provider root",
    );
  }
  return (packageName, issuerPackageJsonPath) => {
    const canonicalIssuer =
      issuerPackageJsonPath === undefined
        ? canonicalManifest
        : realpathSync(issuerPackageJsonPath);
    if (!pathIsInside(canonicalRoot, canonicalIssuer)) {
      throw new Error(
        `ACPX provider package issuer for ${packageName} resolves outside the selected provider root`,
      );
    }
    const packageJsonPath = realpathSync(
      resolvePackageJsonFromIssuer(packageName, canonicalIssuer),
    );
    if (!pathIsInside(canonicalNodeModules, packageJsonPath)) {
      throw new Error(

View on GitHub (pinned to 3f1d897a7c)