{"record":{"id":"590610b72c0342cb","repo":"paperclipai/paperclip","slug":"additional-source-projectid-is-not-a-simple-path-s-590610","errorCode":null,"errorMessage":"additional source projectId is not a simple path segment: ${projectId}","messagePattern":"additional source projectId is not a simple path segment: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/adapter-utils/src/sandbox-managed-runtime.ts","lineNumber":1019,"sourceCode":"    // their own roots. No workspace, git-history, or `.paperclip-runtime`\n    // semantics apply; those stay anchor-only. Per-project failure isolation: one\n    // project's confinement or sync failure logs a warning and is skipped, and\n    // the run plus the other projects continue. Only a project that stages\n    // successfully appears in `additionalSourceDirs`.\n    for (const source of input.additionalSources ?? []) {\n      const { localPath, projectId } = source;\n      const label = `project-${projectId}`;\n      try {\n        if (!path.posix.isAbsolute(localPath)) {\n          throw new Error(`additional source localPath is not an absolute path: ${localPath}`);\n        }\n        if (\n          projectId.length === 0 ||\n          projectId.includes(\"/\") ||\n          projectId.includes(\"\\\\\") ||\n          projectId.includes(\"..\")\n        ) {\n          throw new Error(`additional source projectId is not a simple path segment: ${projectId}`);\n        }\n        const remoteProjectDir = path.posix.join(runtimeRootDir, label);\n        await emitRuntimeStatus(input.onRuntimeProgress, \"config_sync\", \"Syncing referenced project to sandbox\");\n        await stageConfinedSyncIn({\n          files: [{\n            sourcePath: localPath,\n            targetPath: remoteProjectDir,\n            kind: \"directory\",\n            exclude: additionalSourceExclude,\n            access: \"ro\",\n          }],\n          sourceRoots: [localPath],\n          targetRoots: [remoteProjectDir],\n          progressLabel: label,\n          statusPhase: \"config_sync\",\n          progressBytes: 0,\n        });\n        additionalSourceDirs[projectId] = remoteProjectDir;","sourceCodeStart":1001,"sourceCodeEnd":1037,"githubUrl":"https://github.com/paperclipai/paperclip/blob/67001ec6eb96ae601aa27bc91d9b2415d665334a/packages/adapter-utils/src/sandbox-managed-runtime.ts#L1001-L1037","documentation":"Thrown during additional-source processing when projectId is empty, or contains a slash, backslash, or '..'. The projectId is used directly in a label and joined into a remote directory path (runtimeRootDir/project-<id>), so it must be a single safe path segment; otherwise it would enable path injection or directory ambiguity. Caught per-source so one bad project does not abort the run.","triggerScenarios":"An additionalSources entry with projectId = '', 'a/b', 'a\\b', or '..'. The check also rejects backslashes to keep POSIX remote paths safe.","commonSituations":"projectId sourced from an unvalidated user input or repo name with a slash; a UUID-style id that accidentally includes a path separator; a default empty string when the field was optional and not set; a multi-segment org/repo slug used verbatim.","solutions":["Generate projectId from a safe charset (alphanumeric, dash, underscore) — e.g. slugify the repo name.","Validate projectId with /^[A-Za-z0-9_-]+$/ at the collection boundary before passing to prepareSandboxManagedRuntime.","For org/repo inputs, join with a non-separator like '-' (e.g. 'org-repo') rather than '/'.","Ensure projectId is always populated (non-empty) for referenced projects."],"exampleFix":"// before\nadditionalSources: [{ localPath: abs, projectId: 'acme/widgets' }]\n// after\nadditionalSources: [{ localPath: abs, projectId: 'acme-widgets' }]","handlingStrategy":"validation","validationCode":"const PROJECT_ID_RE = /^[A-Za-z0-9_-]+$/;\nfor (const s of additionalSources ?? []) {\n  if (!PROJECT_ID_RE.test(s.projectId)) {\n    throw new Error(`additionalSource projectId must be a safe segment: ${s.projectId}`);\n  }\n}","typeGuard":"function isSafeProjectId(id: string): boolean {\n  return typeof id === 'string' && id.length > 0 && /^[A-Za-z0-9_-]+$/.test(id);\n}","tryCatchPattern":null,"preventionTips":["Slugify any org/repo input into [A-Za-z0-9_-] before using it as projectId.","Make projectId a required field in the additional-source schema.","Reject empty/slash/backslash ids at the API boundary."],"tags":["sandbox","additional-sources","security","path-segment","validation"],"backgroundTag":null,"analyzedSha":"67001ec6eb96ae601aa27bc91d9b2415d665334a","analyzedAt":"2026-08-12T12:05:45.408Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}