{"record":{"id":"94174a915a98e14b","repo":"paperclipai/paperclip","slug":"workspace-file-must-have-a-canonical-absolute-path","errorCode":null,"errorMessage":"Workspace file must have a canonical absolute path","messagePattern":"Workspace file must have a canonical absolute path","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/src/services/native-runtime/runner-api-files.ts","lineNumber":7,"sourceCode":"import { constants } from \"node:fs\";\nimport { open, type FileHandle } from \"node:fs/promises\";\nimport { isAbsolute } from \"node:path\";\n\n/** Open a previously authorized canonical path without following raced symlinks. */\nexport async function openRunnerApiWorkspaceFile(path: string): Promise<FileHandle> {\n  if (!isAbsolute(path)) throw new Error(\"Workspace file must have a canonical absolute path\");\n  if (process.platform === \"darwin\") {\n    // Darwin sys/fcntl.h: O_NOFOLLOW_ANY rejects symlinks at every component.\n    // Node does not expose this flag in fs.constants. Unsupported kernels fail\n    // closed instead of falling back to a pathname check followed by open.\n    return open(path, constants.O_RDONLY | constants.O_NONBLOCK | 0x20000000);\n  }\n  if (process.platform !== \"linux\") throw new Error(\"Workspace uploads require a platform with confined file opens; use an authorized artifact reference\");\n  const parts = path.split(\"/\").filter(Boolean);\n  if (!parts.length || parts.some(part => part === \".\" || part === \"..\")) throw new Error(\"Invalid canonical workspace path\");\n  let directory = await open(\"/\", constants.O_RDONLY | constants.O_DIRECTORY);\n  try {\n    for (const part of parts.slice(0, -1)) {\n      // Linux magic descriptor links provide openat-style directory confinement.\n      const next = await open(`/proc/self/fd/${directory.fd}/${part}`, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);\n      await directory.close();\n      directory = next;\n    }\n    return await open(`/proc/self/fd/${directory.fd}/${parts.at(-1)}`, constants.O_RDONLY | constants.O_NONBLOCK | constants.O_NOFOLLOW);","sourceCodeStart":1,"sourceCodeEnd":25,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/server/src/services/native-runtime/runner-api-files.ts#L1-L25","documentation":"openRunnerApiWorkspaceFile opens a previously authorized workspace file for runner API uploads without following raced symlinks. It requires an absolute canonical path; a relative path fails immediately with \"Workspace file must have a canonical absolute path\". This is a confinement precondition — the caller must pass the exact absolute path that was authorized.","triggerScenarios":"A runner file-upload tool call supplies a workspace file path that is relative (e.g. \"src/index.ts\", \"./file.txt\") instead of an absolute canonical path like \"/workspace/src/index.ts\".","commonSituations":"Agent resolves a file against its own cwd and passes the relative form; path built by string concatenation without path.resolve; LLM tool argument omits the workspace root prefix; hand-written scripts inside the run calling the API with relative paths.","solutions":["Convert the path to an absolute canonical form with path.resolve(workspaceRoot, relativePath) before the upload call.","Use the exact absolute path returned by the authorization step that admitted the file.","Reject/normalize leading \"./\" or env-relative segments on the agent side before calling the runner tool.","If the file truly has no workspace root, use an authorized artifact reference instead of a workspace path."],"exampleFix":"// before\nawait openRunnerApiWorkspaceFile(\"src/report.pdf\");\n// after\nimport { resolve } from \"node:path\";\nawait openRunnerApiWorkspaceFile(resolve(\"/workspace\", \"src/report.pdf\"));","handlingStrategy":"validation","validationCode":"import { isAbsolute } from \"node:path\";\nfunction ensureCanonicalWorkspacePath(p: string): string {\n  if (!isAbsolute(p)) throw new Error(`workspace file path must be absolute, got: ${p}`);\n  return p;\n}\n// call before invoking any upload that reaches openRunnerApiWorkspaceFile","typeGuard":"function isAbsoluteWorkspacePath(p: string): boolean { return isAbsolute(p); }","tryCatchPattern":"try {\n  const handle = await openRunnerApiWorkspaceFile(path);\n} catch (err) {\n  if (err instanceof Error && err.message === \"Workspace file must have a canonical absolute path\") {\n    // convert to absolute via path.resolve(workspaceRoot, path) and retry once\n  } else throw err;\n}","preventionTips":["Have the tool layer normalize every agent-supplied path with path.resolve(workspaceRoot, input) before use.","Pass forward the absolute path returned by the file-authorization step, not the agent's original string.","Log the offending path in the error path to make relative-path bugs obvious."],"tags":["filesystem","path-validation","security","native-runtime"],"backgroundTag":"invalid-argument-format","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}