{"record":{"id":"257199d5fb880bfb","repo":"affaan-m/ECC","slug":"symlinks-are-not-accepted-part","errorCode":null,"errorMessage":"Symlinks are not accepted: {part}","messagePattern":"Symlinks are not accepted: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"skills/taste-application/scripts/tasteforge/assets.py","lineNumber":39,"sourceCode":"\n\ndef _text(value: Any, name: str) -> str:\n    if not isinstance(value, str) or not value.strip():\n        raise ValueError(f'{name} must be a nonempty string')\n    return value\n\n\ndef _path(value: Any, base: Path) -> Path:\n    raw = _text(str(value) if isinstance(value, Path) else value, 'path')\n    if '://' in raw or raw.startswith(('file:', 'http:', 'https:')):\n        raise ValueError('Only local filesystem paths are supported')\n    path = Path(raw).expanduser()\n    if not path.is_absolute():\n        path = base / path\n    # Check before resolving '..' to avoid hiding a symlink in the path.\n    for part in (path, *path.parents):\n        if part.is_symlink():\n            raise ValueError(f'Symlinks are not accepted: {part}')\n    return path.resolve()\n\n\ndef _fingerprint(path: Path, modality: str | None = None) -> dict[str, Any]:\n    _path(path, Path.cwd())\n    try:\n        before = path.stat()\n        if not stat.S_ISREG(before.st_mode):\n            raise ValueError(f'Not a regular file: {path}')\n        flags = os.O_RDONLY | getattr(os, 'O_NOFOLLOW', 0) | os.O_NONBLOCK\n        fd = os.open(path, flags)\n        with os.fdopen(fd, 'rb') as stream:\n            opened = os.fstat(stream.fileno())\n            if not stat.S_ISREG(opened.st_mode):\n                raise ValueError(f'Not a regular file: {path}')\n            digest = hashlib.sha256()\n            header = stream.read(12)\n            digest.update(header)","sourceCodeStart":21,"sourceCodeEnd":57,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/skills/taste-application/scripts/tasteforge/assets.py#L21-L57","documentation":"_path walks the path and all of its parents and refuses any component that is a symlink, before resolving '..'. This prevents symlink-based path traversal and ensures fingerprints/provenance bind to the real file location. Any symlinked component raises ValueError naming the offending path component.","triggerScenarios":"Passing a path through a symlinked directory (e.g. /tmp being a symlink to /private/tmp on macOS, or ~/assets pointing elsewhere) to ingest_assets/validate_assets/_bundle/_provenance.","commonSituations":"macOS /tmp → /private/tmp; home directories symlinked into cloud-sync folders; dotfile-managed directories (stow) that are symlinks; project dirs reached via a convenience symlink.","solutions":["Use path.resolve() (or os.path.realpath) to get the real path and pass that instead.","Move or copy assets into a non-symlinked location and reference them directly.","On macOS, use /private/tmp instead of /tmp when staging assets.","Check which component is flagged in the message and replace that symlink with a real directory."],"exampleFix":"// before\ningest_assets(sp, paths=[\"/tmp/renders/clip.mp4\"])  # /tmp is a symlink\n\n// after\nfrom pathlib import Path\nreal = Path(\"/tmp/renders/clip.mp4\").resolve()  # e.g. /private/tmp/renders/clip.mp4\ningest_assets(sp, paths=[str(real)])","handlingStrategy":"validation","validationCode":"from pathlib import Path\ndef assert_no_symlinks(p: str):\n    path = Path(p).expanduser()\n    for part in (path, *path.parents):\n        if part.is_symlink():\n            raise ValueError(f\"symlink component: {part}\")","typeGuard":"def is_symlink_free(p: str) -> bool:\n    from pathlib import Path\n    path = Path(p).expanduser()\n    return not any(part.is_symlink() for part in (path, *path.parents))","tryCatchPattern":"try:\n    assets = ingest_assets(sp, paths=paths)\nexcept ValueError as e:\n    if \"Symlinks are not accepted\" in str(e):\n        paths = [str(Path(p).resolve()) for p in paths]\n        assets = ingest_assets(sp, paths=paths)\n    else:\n        raise","preventionTips":["Always pass Path(...).resolve() outputs to the asset API.","On macOS, stage assets under /private/tmp, not /tmp.","Avoid symlink-farm project layouts (e.g. stow) for asset directories."],"tags":["symlink","path-safety","security"],"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"}