{"record":{"id":"a50ec1f0d572f471","repo":"affaan-m/ECC","slug":"output-bundle-root-must-not-be-a-symlink","errorCode":null,"errorMessage":"output bundle root must not be a symlink","messagePattern":"output bundle root must not be a symlink","errorType":"validation","errorClass":"ContractError","httpStatus":null,"severity":"error","filePath":"skills/taste-application/scripts/tasteforge/contract.py","lineNumber":101,"sourceCode":"            digest.update(chunk)\n    finally:\n        os.close(descriptor)\n    return digest.hexdigest()\n\n\ndef _semantic_signature(spec: dict[str, Any]) -> str:\n    signature = spec.get(\"signature\", {})\n    return json.dumps(signature, sort_keys=True, separators=(\",\", \":\"))\n\n\ndef _validate_output_tree(root: Path) -> None:\n    \"\"\"Reject symlinks and special files before parsing bundle content.\"\"\"\n    try:\n        metadata = root.lstat()\n    except FileNotFoundError:\n        raise ContractError(\"output bundle is missing\") from None\n    if stat.S_ISLNK(metadata.st_mode):\n        raise ContractError(\"output bundle root must not be a symlink\")\n    if not stat.S_ISDIR(metadata.st_mode):\n        raise ContractError(\"output bundle root must be a directory\")\n    pending = [root]\n    while pending:\n        directory = pending.pop()\n        with os.scandir(directory) as entries:\n            for entry in entries:\n                if entry.is_symlink():\n                    raise ContractError(f\"output bundle contains a symlink: {entry.path}\")\n                if entry.is_dir(follow_symlinks=False):\n                    pending.append(Path(entry.path))\n                elif not entry.is_file(follow_symlinks=False):\n                    raise ContractError(f\"output bundle contains a special file: {entry.path}\")\n\n\ndef validate_genre_specs(specs: list[dict[str, Any]]) -> None:\n    \"\"\"Require complete, semantically distinct numbered genre specs.\"\"\"\n    if not specs:","sourceCodeStart":83,"sourceCodeEnd":119,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/skills/taste-application/scripts/tasteforge/contract.py#L83-L119","documentation":"_validate_output_tree rejects a bundle root that is itself a symbolic link. The contract requires the output bundle to be a real directory so the validation walks actual bundle contents rather than following a link that could be redirected (a security/TOCTOU measure).","triggerScenarios":"Calling validate_bundle with a path that resolves through a symlink — e.g. a symlinked output dir in a workspace, or linking the bundle into another location before validation.","commonSituations":"CI workspaces that symlink artifact directories, developers symlinking outputs from a scratch disk into the repo, macOS temp-dir symlinks (/tmp -> /private/tmp) when the bundle is staged in temp storage.","solutions":["Create/point to the real directory instead of the symlink","Copy or bind-mount (or use a hard link to the directory contents) rather than symlinking","Adjust the pipeline to validate at the bundle's real location before linking","On macOS, avoid staging under /tmp symlinks; use the resolved /private path or a real directory"],"exampleFix":"# before\nln -s /scratch/build-out dist/bundle\nvalidate_bundle(Path('dist/bundle'))\n# after\ncp -r /scratch/build-out dist/bundle\nvalidate_bundle(Path('dist/bundle'))","handlingStrategy":"validation","validationCode":"import os, stat\nmd = os.lstat(bundle_root)\nassert not stat.S_ISLNK(md.st_mode), 'bundle root must not be a symlink'","typeGuard":"def is_real_dir(path) -> bool:\n    import os, stat\n    try:\n        return stat.S_ISDIR(os.lstat(path).st_mode)\n    except OSError:\n        return False","tryCatchPattern":"try:\n    validate_bundle(root)\nexcept ContractError as e:\n    if 'must not be a symlink' in str(e):\n        print(f'Replace the symlink at {root} with a real directory')\n    else:\n        raise","preventionTips":["Never symlink bundle outputs into place; copy instead","Resolve paths (Path.resolve()) but validate the real target","Avoid staging bundles under /tmp-style symlinked dirs on macOS"],"tags":["filesystem","symlink","security"],"backgroundTag":"incompatible-source-type","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"}