{"record":{"id":"ca0489b84f0f685e","repo":"paperclipai/paperclip","slug":"acpx-provider-node-modules-resolves-outside-the-se","errorCode":null,"errorMessage":"ACPX provider node_modules resolves outside the selected provider root","messagePattern":"ACPX provider node_modules resolves outside the selected provider root","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/src/drivers/acpx/installation-integrity.ts","lineNumber":323,"sourceCode":"    manifest.includes(\"\\0\") ||\n    resolve(manifest) !== manifest\n  ) {\n    throw new Error(\n      \"ACPX provider package manifest must be an explicit normalized absolute path\",\n    );\n  }\n  const canonicalRoot = realpathSync(root);\n  const canonicalManifest = realpathSync(manifest);\n  if (!pathIsInside(canonicalRoot, canonicalManifest)) {\n    throw new Error(\n      \"ACPX provider package manifest resolves outside the selected provider root\",\n    );\n  }\n  const canonicalNodeModules = realpathSync(\n    resolve(canonicalRoot, \"node_modules\"),\n  );\n  if (!pathIsInside(canonicalRoot, canonicalNodeModules)) {\n    throw new Error(\n      \"ACPX provider node_modules resolves outside the selected provider root\",\n    );\n  }\n  return (packageName, issuerPackageJsonPath) => {\n    const canonicalIssuer =\n      issuerPackageJsonPath === undefined\n        ? canonicalManifest\n        : realpathSync(issuerPackageJsonPath);\n    if (!pathIsInside(canonicalRoot, canonicalIssuer)) {\n      throw new Error(\n        `ACPX provider package issuer for ${packageName} resolves outside the selected provider root`,\n      );\n    }\n    const packageJsonPath = realpathSync(\n      resolvePackageJsonFromIssuer(packageName, canonicalIssuer),\n    );\n    if (!pathIsInside(canonicalNodeModules, packageJsonPath)) {\n      throw new Error(","sourceCodeStart":305,"sourceCodeEnd":341,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/src/drivers/acpx/installation-integrity.ts#L305-L341","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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)","Use pnpm's node-linker=hoisted in .npmrc for the provider install to avoid symlinked layouts","Point the provider root at the directory whose realpath actually contains node_modules","In containers, mount/copy node_modules inside the provider root rather than linking from outside"],"exampleFix":"// before\nln -s /store/acpx-provider/node_modules /opt/acpx/provider/node_modules\n// after\n# .npmrc in provider root\nnode-linker=hoisted\n$ npm install --prefix /opt/acpx/provider   # real node_modules inside root","handlingStrategy":"validation","validationCode":"import fs from 'node:fs';\nimport path from 'node:path';\nfunction nodeModulesInsideRoot(root: string): boolean {\n  const canonicalRoot = fs.realpathSync(root);\n  const nm = fs.realpathSync(path.resolve(canonicalRoot, 'node_modules'));\n  return nm.startsWith(canonicalRoot + path.sep);\n}","typeGuard":null,"tryCatchPattern":"try {\n  const resolver = createAcpxPackageJsonResolver(root);\n} catch (err) {\n  if (err instanceof Error && /node_modules resolves outside the selected provider root/.test(err.message)) {\n    throw new Error(`Provider ${root} uses a symlinked node_modules store; reinstall with node-linker=hoisted or copy node_modules into the provider root`);\n  }\n  throw err;\n}","preventionTips":["Prefer npm/yarn installs for provider packages; if pnpm, set node-linker=hoisted in the provider's .npmrc","Check a staged provider install for escaping symlinks (find root -type l) before registering it","In containers, copy node_modules into the provider root instead of bind-mounting from outside","Document that the provider root must be a self-contained real directory"],"tags":["acpx","path-validation","node-modules","symlink","security"],"backgroundTag":"path-traversal-blocked","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}