{"record":{"id":"305c69c0ec77c0f6","repo":"affaan-m/ECC","slug":"source-is-missing-the-expected-catalog-marker","errorCode":null,"errorMessage":"${source} is missing the expected catalog marker","messagePattern":"(.+?) is missing the expected catalog marker","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/ci/catalog.js","lineNumber":82,"sourceCode":"function readFileOrThrow(filePath) {\n  try {\n    return fs.readFileSync(filePath, 'utf8');\n  } catch (error) {\n    throw new Error(`Failed to read ${path.basename(filePath)}: ${error.message}`);\n  }\n}\n\nfunction writeFileOrThrow(filePath, content) {\n  try {\n    fs.writeFileSync(filePath, content, 'utf8');\n  } catch (error) {\n    throw new Error(`Failed to write ${path.basename(filePath)}: ${error.message}`);\n  }\n}\n\nfunction replaceOrThrow(content, regex, replacer, source) {\n  if (!regex.test(content)) {\n    throw new Error(`${source} is missing the expected catalog marker`);\n  }\n\n  return content.replace(regex, replacer);\n}\n\nfunction parseReadmeExpectations(readmeContent) {\n  const expectations = [];\n\n  const quickStartMatch = readmeContent.match(\n    /access to\\s+(\\d+)\\s+agents,\\s+(\\d+)\\s+skills,\\s+and\\s+(\\d+)\\s+(?:commands|legacy command shims?)/i\n  );\n  if (!quickStartMatch) {\n    throw new Error('README.md is missing the quick-start catalog summary');\n  }\n\n  expectations.push(\n    { category: 'agents', mode: 'exact', expected: Number(quickStartMatch[1]), source: 'README.md quick-start summary' },\n    { category: 'skills', mode: 'exact', expected: Number(quickStartMatch[2]), source: 'README.md quick-start summary' },","sourceCodeStart":64,"sourceCodeEnd":100,"githubUrl":"https://github.com/affaan-m/ECC/blob/01e15490f04e29cfefe3896951f43db46994d8ee/scripts/ci/catalog.js#L64-L100","documentation":"_remove_project_storage() is a defense-in-depth guard around shutil.rmtree. It resolves both PROJECTS_DIR and the target (PROJECTS_DIR / project_id) and refuses to delete if the resolved target equals the root itself or is not a descendant of it. This prevents a path-traversal project_id (or a symlinked project directory, or a future caller with a relaxed validator) from turning the recursive delete into an arbitrary-directory removal.","triggerScenarios":"project_id contains '../' that resolves outside PROJECTS_DIR; project_id is empty or '.' so project_dir == projects_root; a symlink inside PROJECTS_DIR/<id> points outside the root; project_id contains an absolute path.","commonSituations":"Upstream project_id validation was bypassed or relaxed; a symlink was placed in the projects directory; an empty/None project_id reached this function; a test injected a traversal string.","solutions":["Validate project_id upstream with _validate_instinct_id (alphanumeric + ._-, no '/', '\\', or '..').","Remove any symlinks inside PROJECTS_DIR before calling, or reject symlinked project directories.","Never pass an empty, None, or absolute-path project_id to this function."],"exampleFix":"# before\n_remove_project_storage(project_id)  # project_id = '../evil'\n\n# after\nif not _validate_instinct_id(project_id):\n    raise ValueError(f'invalid project id: {project_id!r}')\n_remove_project_storage(project_id)","handlingStrategy":"validation","validationCode":"# Validate the project_id before calling _remove_project_storage.\nif not _validate_instinct_id(project_id):\n    raise ValueError(f'refusing to remove storage: invalid project_id {project_id!r}')\n_remove_project_storage(project_id)","typeGuard":"# Reuse the existing validator as a type guard.\nfrom pathlib import Path\n\ndef is_safe_project_id(project_id: str) -> bool:\n    return (\n        bool(project_id)\n        and len(project_id) <= 128\n        and '/' not in project_id\n        and '\\\\' not in project_id\n        and '..' not in project_id\n        and not project_id.startswith('.')\n    )","tryCatchPattern":"try:\n    _remove_project_storage(project_id)\nexcept ValueError as e:\n    if 'escapes' in str(e):\n        log.error('refused to delete: project_id resolves outside PROJECTS_DIR — possible traversal')\n    raise","preventionTips":["Always validate project_id with _validate_instinct_id before any storage operation.","Reject empty, None, or absolute-path project IDs at the input boundary.","Periodically check for and remove symlinks inside PROJECTS_DIR.","Treat an 'escapes' error as a security incident, not a routine failure."],"tags":["security","filesystem","path-traversal","destructive","validation"],"backgroundTag":null,"analyzedSha":"01e15490f04e29cfefe3896951f43db46994d8ee","analyzedAt":"2026-08-13T00:31:08.655Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}