{"record":{"id":"4d3a5c73095f53cc","repo":"affaan-m/ECC","slug":"refusing-unsafe-repair-source-metadata-sources-must-stay","errorCode":null,"errorMessage":"Refusing unsafe repair source metadata: sources must stay within the repository.","messagePattern":"Refusing unsafe repair source metadata: sources must stay within the repository\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"scripts/lib/install-lifecycle.js","lineNumber":148,"sourceCode":"  const stdout = typeof error.stdout === 'string' ? error.stdout.trim() : '';\n  return stderr || stdout || error.message || 'Failed to build OpenCode payload';\n}\n\nfunction getManagedOperations(state) {\n  return Array.isArray(state && state.operations) ? state.operations.filter(operation => operation.ownership === 'managed') : [];\n}\n\nfunction createUnsafeRepairSourceError() {\n  return new Error(\n    'Refusing unsafe repair source metadata: sources must stay within the repository.'\n  );\n}\n\nfunction assertSafeRepairSourcePath(sourcePath, repoRoot) {\n  try {\n    return assertWithinTrustedRoot(sourcePath, repoRoot, 'read repair source');\n  } catch {\n    throw createUnsafeRepairSourceError();\n  }\n}\n\nfunction resolveOperationSourcePath(repoRoot, operation) {\n  if (operation.sourceRelativePath) {\n    if (typeof operation.sourceRelativePath !== 'string') {\n      throw createUnsafeRepairSourceError();\n    }\n\n    const sourceRelativePath = operation.sourceRelativePath;\n    const hasParentTraversal = sourceRelativePath\n      .split(/[/\\\\]+/)\n      .includes('..');\n    const isAbsolute = path.isAbsolute(sourceRelativePath)\n      || path.win32.isAbsolute(sourceRelativePath);\n    if (isAbsolute || hasParentTraversal) {\n      throw createUnsafeRepairSourceError();\n    }","sourceCodeStart":130,"sourceCodeEnd":166,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/scripts/lib/install-lifecycle.js#L130-L166","documentation":"Error raised by assertSafeRepairSourcePath in scripts/lib/install-lifecycle.js:148 (via createUnsafeRepairSourceError). Before a repair/hydration operation reads a recorded source file, the path is checked with assertWithinTrustedRoot against the repository root. If the recorded repair source resolves outside the repo (or the trust check itself throws), the lifecycle layer refuses the read with the message that repair sources must stay within the repository, preventing operations recorded with escaped or absolute foreign paths from being replayed.","triggerScenarios":"Replaying or inspecting a recorded install/repair operation whose sourcePath fails assertWithinTrustedRoot — e.g. the recorded path is absolute outside the repo (`/etc/...`, `C:\\...`), or the trust check throws (non-existent path, symlink escape) and the catch re-raises as the unsafe-repair-source error.","commonSituations":"A manifest/record file was hand-edited or generated by an older tool version storing absolute paths; the repo was moved/renamed so previously relative-trusted paths now resolve outside the new root; symlinked source directories pointing outside the repository.","solutions":["Inspect the operation record's sourceRelativePath/sourcePath and rewrite it as a path relative to the repository root.","Confirm the referenced file actually exists inside the repo; re-record the operation if the original file was moved.","Re-generate the operations manifest with the current ECC tooling instead of editing recorded paths by hand.","If symlinks are involved, replace them with real in-repo copies so the trust check resolves within the root."],"exampleFix":"// before (recorded operation)\n{ \"sourceRelativePath\": \"../../outside/secret-file.md\" }\n\n// after\n{ \"sourceRelativePath\": \"agents/code-reviewer.md\" }","handlingStrategy":"validation","validationCode":"const path = require('path');\nfunction isWithinRepo(rel, repoRoot) {\n  if (typeof rel !== 'string') return false;\n  const resolved = path.resolve(repoRoot, rel);\n  return resolved.startsWith(path.resolve(repoRoot) + path.sep);\n}\nif (!isWithinRepo(op.sourceRelativePath, repoRoot)) throw new Error('repair source outside repo');","typeGuard":"function isSafeRepairSource(op, repoRoot) {\n  return typeof op.sourceRelativePath === 'string' &&\n    !op.sourceRelativePath.split(/[/\\\\]+/).includes('..') &&\n    path.resolve(repoRoot, op.sourceRelativePath).startsWith(path.resolve(repoRoot));\n}","tryCatchPattern":"try { const src = resolveOperationSourcePath(repoRoot, op); }\ncatch (e) {\n  console.error('Recorded repair source is unsafe; re-record the operation with an in-repo relative path.');\n  process.exitCode = 1;\n}","preventionTips":["Never hand-edit recorded operation manifests; regenerate them with ECC tooling.","Store only repo-relative paths in sourceRelativePath fields.","Re-record operations after moving or renaming the repository.","Avoid symlinks in repair source directories that escape the repo root."],"tags":["security","path-traversal","install-lifecycle","validation"],"backgroundTag":"path-traversal-blocked","analyzedSha":"8321021c54d670126ce3b2969d5deb880b4b0c2a","analyzedAt":"2026-09-16T10:08:13.343Z","contentChangedAt":"2026-09-16T10:08:13.343Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}