affaan-m/ECC · error · Error

${source} is missing the expected catalog marker

Error message

${source} is missing the expected catalog marker

What it means

_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.

Source

Thrown at scripts/ci/catalog.js:82

function readFileOrThrow(filePath) {
  try {
    return fs.readFileSync(filePath, 'utf8');
  } catch (error) {
    throw new Error(`Failed to read ${path.basename(filePath)}: ${error.message}`);
  }
}

function writeFileOrThrow(filePath, content) {
  try {
    fs.writeFileSync(filePath, content, 'utf8');
  } catch (error) {
    throw new Error(`Failed to write ${path.basename(filePath)}: ${error.message}`);
  }
}

function replaceOrThrow(content, regex, replacer, source) {
  if (!regex.test(content)) {
    throw new Error(`${source} is missing the expected catalog marker`);
  }

  return content.replace(regex, replacer);
}

function parseReadmeExpectations(readmeContent) {
  const expectations = [];

  const quickStartMatch = readmeContent.match(
    /access to\s+(\d+)\s+agents,\s+(\d+)\s+skills,\s+and\s+(\d+)\s+(?:commands|legacy command shims?)/i
  );
  if (!quickStartMatch) {
    throw new Error('README.md is missing the quick-start catalog summary');
  }

  expectations.push(
    { category: 'agents', mode: 'exact', expected: Number(quickStartMatch[1]), source: 'README.md quick-start summary' },
    { category: 'skills', mode: 'exact', expected: Number(quickStartMatch[2]), source: 'README.md quick-start summary' },

View on GitHub (pinned to 01e15490f0)

Solutions

  1. Validate project_id upstream with _validate_instinct_id (alphanumeric + ._-, no '/', '\', or '..').
  2. Remove any symlinks inside PROJECTS_DIR before calling, or reject symlinked project directories.
  3. Never pass an empty, None, or absolute-path project_id to this function.

Example fix

# before
_remove_project_storage(project_id)  # project_id = '../evil'

# after
if not _validate_instinct_id(project_id):
    raise ValueError(f'invalid project id: {project_id!r}')
_remove_project_storage(project_id)
Defensive patterns

Strategy: validation

Validate before calling

# Validate the project_id before calling _remove_project_storage.
if not _validate_instinct_id(project_id):
    raise ValueError(f'refusing to remove storage: invalid project_id {project_id!r}')
_remove_project_storage(project_id)

Type guard

# Reuse the existing validator as a type guard.
from pathlib import Path

def is_safe_project_id(project_id: str) -> bool:
    return (
        bool(project_id)
        and len(project_id) <= 128
        and '/' not in project_id
        and '\\' not in project_id
        and '..' not in project_id
        and not project_id.startswith('.')
    )

Try / catch

try:
    _remove_project_storage(project_id)
except ValueError as e:
    if 'escapes' in str(e):
        log.error('refused to delete: project_id resolves outside PROJECTS_DIR — possible traversal')
    raise

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13). Data as JSON: /api/errors/305c69c0ec77c0f6. Report an issue: GitHub.