affaan-m/ECC · error · ContractError
output bundle root must not be a symlink
Error message
output bundle root must not be a symlink
What it means
_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).
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
Example fix
# before
ln -s /scratch/build-out dist/bundle
validate_bundle(Path('dist/bundle'))
# after
cp -r /scratch/build-out dist/bundle
validate_bundle(Path('dist/bundle')) Defensive patterns
Strategy: validation
Validate before calling
import os, stat md = os.lstat(bundle_root) assert not stat.S_ISLNK(md.st_mode), 'bundle root must not be a symlink'
Type guard
def is_real_dir(path) -> bool:
import os, stat
try:
return stat.S_ISDIR(os.lstat(path).st_mode)
except OSError:
return False Try / catch
try:
validate_bundle(root)
except ContractError as e:
if 'must not be a symlink' in str(e):
print(f'Replace the symlink at {root} with a real directory')
else:
raise Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "is not a compatible type" / "cannot merge" errors: when a value's type doesn't match what the library requires — this error's family across 65 libraries.
Related errors
- gate.variant_invalid
- output artifact must be a regular file
- output bundle contains a symlink
- output destination must not be a symlink
- output directory must not be a symlink
AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16).
Data as JSON: /api/errors/a50ec1f0d572f471.
Report an issue: GitHub.
Appendix: source
Thrown at skills/taste-application/scripts/tasteforge/contract.py:101
digest.update(chunk)
finally:
os.close(descriptor)
return digest.hexdigest()
def _semantic_signature(spec: dict[str, Any]) -> str:
signature = spec.get("signature", {})
return json.dumps(signature, sort_keys=True, separators=(",", ":"))
def _validate_output_tree(root: Path) -> None:
"""Reject symlinks and special files before parsing bundle content."""
try:
metadata = root.lstat()
except FileNotFoundError:
raise ContractError("output bundle is missing") from None
if stat.S_ISLNK(metadata.st_mode):
raise ContractError("output bundle root must not be a symlink")
if not stat.S_ISDIR(metadata.st_mode):
raise ContractError("output bundle root must be a directory")
pending = [root]
while pending:
directory = pending.pop()
with os.scandir(directory) as entries:
for entry in entries:
if entry.is_symlink():
raise ContractError(f"output bundle contains a symlink: {entry.path}")
if entry.is_dir(follow_symlinks=False):
pending.append(Path(entry.path))
elif not entry.is_file(follow_symlinks=False):
raise ContractError(f"output bundle contains a special file: {entry.path}")
def validate_genre_specs(specs: list[dict[str, Any]]) -> None:
"""Require complete, semantically distinct numbered genre specs."""
if not specs:View on GitHub (pinned to 8321021c54)