{"record":{"id":"d1610f7b4fff3906","repo":"santifer/career-ops","slug":"label-could-not-be-canonicalized-against-the-tracker","errorCode":null,"errorMessage":"${label} could not be canonicalized against the tracker workspace (${/** @type {any} */ (err)?.code || 'realpath failed'} on ${probe}): ${absPath}","messagePattern":"(.+?) could not be canonicalized against the tracker workspace \\((.+?) \\*/ \\(err\\)\\?\\.code \\|\\| 'realpath failed'\\} on (.+?)\\): (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"generate-pdf.mjs","lineNumber":119,"sourceCode":"  let probe = absPath;\n  const tail = [];\n  while (!existsSync(probe)) {\n    tail.unshift(basename(probe));\n    const parent = dirname(probe);\n    if (parent === probe) break; // reached the filesystem root\n    probe = parent;\n  }\n  let canonical;\n  try {\n    canonical = existsSync(probe) ? resolve(realpathSync(probe), ...tail) : absPath;\n  } catch (err) {\n    // Canonicalization failed (realpath raced away, permission error): containment\n    // is unprovable, so fail closed rather than fall back to a lexical form that a\n    // symlinked ancestor could slip past. Named as its own failure, not as an\n    // escape: an intermittent CI-only hit of this guard (#3162) was undiagnosable\n    // while both branches threw the same message — \"escapes\" points a reader at\n    // the path, when the actual event was realpath failing underneath it.\n    throw new Error(\n      `${label} could not be canonicalized against the tracker workspace`\n      + ` (${/** @type {any} */ (err)?.code || 'realpath failed'} on ${probe}): ${absPath}`,\n    );\n  }\n  const workspace = canonicalWorkspaceRoot();\n  const rel = relative(workspace, canonical);\n  if (rel === '' || rel.startsWith('..') || isAbsolute(rel)) {\n    // #3162: an intermittent macOS-CI-only hit of this branch happens on paths\n    // that are lexically inside the sandbox, and canonicalization SUCCEEDS\n    // before it. Print both sides so the next occurrence names the disagreeing\n    // ancestor outright instead of asking a reader to reconstruct it.\n    throw new Error(\n      `${label} escapes the tracker workspace: ${absPath}`\n      + ` (workspaceRoot=${workspace} canonical=${canonical} rel=${rel})`,\n    );\n  }\n  return absPath;\n}","sourceCodeStart":101,"sourceCodeEnd":137,"githubUrl":"https://github.com/santifer/career-ops/blob/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/generate-pdf.mjs#L101-L137","documentation":"generate-pdf.mjs's assertInsideWorkspace proves a manifest input/output path stays inside the tracker workspace by resolving it through realpath. When realpath itself fails (path raced away, permission error), containment cannot be proven, so the guard fails closed with this distinct 'could not be canonicalized' error rather than silently falling back to a lexical check a symlinked ancestor could bypass. It is deliberately named differently from the 'escapes' error so CI failures are diagnosable.","triggerScenarios":"Calling the batch manifest path with an entry whose input path's nearest existing ancestor cannot be realpathed: the file or directory was deleted between the existsSync probe and realpathSync (TOCTOU race), or realpathSync returns EACCES/EPERM on an ancestor directory (common for sandboxed CI runners probing through restricted mount points).","commonSituations":"Intermittent macOS-CI-only hits (issue #3162) where a sandboxed runner denies realpath on a path that is lexically inside the workspace; parallel jobs deleting/renaming temp output directories mid-run; a manifest pointing at a path under a directory the CI user cannot traverse.","solutions":["Re-run the job — if intermittent (a race or CI sandbox hiccup), a retry usually passes","Check permissions on every ancestor directory of the path: the user running generate-pdf must be able to traverse (x) each one","Ensure the path exists and is not being deleted concurrently by another step in the pipeline","If a CI sandbox causes it, exclude the workspace directory from the sandbox's realpath/permission restrictions or run the tool outside the restricted mount","Verify the manifest paths are correct absolute-or-manifest-relative paths inside the tracker workspace"],"exampleFix":"// before (manifest entry pointing into a raced temp dir)\n{\"input\": \"/tmp/runner-tmp/cv/out.html\", \"output\": \"output/cv.pdf\"}\n// after (stable path inside the workspace)\n{\"input\": \"output/cv-tailored.html\", \"output\": \"output/cv-tailored.pdf\"}","handlingStrategy":"try-catch","validationCode":"import { existsSync, statSync } from 'node:fs';\nfunction isReadable(path) {\n  let p = path;\n  while (p && !existsSync(p)) p = p.slice(0, p.lastIndexOf('/')) || '/';\n  try { statSync(p); return true; } catch { return false; }\n}\nif (!isReadable(entryPath)) throw new Error(`unreachable path: ${entryPath}`);","typeGuard":"const isProbedPath = (v) => typeof v === 'string' && v.length > 0 && require('node:path').isAbsolute(v);","tryCatchPattern":"try {\n  assertInsideWorkspace(entryInput, 'input');\n} catch (err) {\n  if (err.message.includes('could not be canonicalized')) {\n    console.error('Path could not be verified (permissions or race):', err.message);\n    console.error('Check ancestor-directory permissions or retry; containment cannot be proven.');\n    process.exit(2);\n  }\n  throw err;\n}","preventionTips":["Ensure the process user has traverse (x) permission on every ancestor of manifest paths","Avoid deleting/renaming input directories while a batch render is running","In CI, run the tool in a workspace directory excluded from sandbox realpath restrictions","Verify manifest paths exist before launching the batch"],"tags":["filesystem","path","security","ci"],"backgroundTag":"realpath-canonicalization-failed","analyzedSha":"aac998c7ed7248ea853b720ceeb1fdbeb322fc5d","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}