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

  1. Create/point to the real directory instead of the symlink
  2. Copy or bind-mount (or use a hard link to the directory contents) rather than symlinking
  3. Adjust the pipeline to validate at the bundle's real location before linking
  4. 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

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


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)