affaan-m/ECC · error · ContractError

receipt source is not a safe regular file

Error message

receipt source is not a safe regular file: {source_path}

What it means

Receipt sources must be safe, regular files: after lstat, the validator rejects anything that is a symbolic link or not a regular file (directories, fifos, sockets, devices) with ContractError('receipt source is not a safe regular file: <path>'). This blocks symlink-based tampering and TOCTOU tricks where a declared source is swapped for a link or special file.

Solutions

  1. Replace the symlink/special file with a real regular file copy at the declared path
  2. Update the receipt's path to point at the actual regular file and regenerate its sha256
  3. Re-run generation after fixing the layout so the receipt matches reality

Example fix

# before
ln -s /shared/data.csv src/data.csv
# after
cp /shared/data.csv src/data.csv  # real regular file
Defensive patterns

Strategy: validation

Validate before calling

import stat
from pathlib import Path

def safe_regular_files(receipt):
    bad = []
    for s in receipt.get('references', []) + receipt.get('evidence_files', []):
        m = Path(s['path']).lstat()
        if stat.S_ISLNK(m.st_mode) or not stat.S_ISREG(m.st_mode):
            bad.append(s['path'])
    return bad  # must be empty

Type guard

def is_safe_regular(path: str) -> bool:
    try:
        m = Path(path).lstat()
    except OSError:
        return False
    return not stat.S_ISLNK(m.st_mode) and stat.S_ISREG(m.st_mode)

Try / catch

try:
    validate_artifact_receipt(receipt, out_dir)
except ContractError as e:
    if 'not a safe regular file' in str(e):
        bad = str(e).rsplit(': ', 1)[1]
        # replace symlink/dir with a real regular file copy, then retry
    raise

Prevention

When it happens

Trigger: A declared source path is a symlink (even pointing to a valid file), a directory, or a special file (fifo/socket/device) at validation time.

Common situations: macOS/Windows checkouts where evidence files became symlinks; users linking sources into a project to avoid duplicating data; a source path pointing at a directory after refactoring; tmpfs/named-pipe intermediates used during generation.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16). Data as JSON: /api/errors/42bdcdbf1ba396e3. Report an issue: GitHub.

Appendix: source

Thrown at skills/taste-application/scripts/tasteforge/contract.py:354

                if float(source_duration) <= 0:
                    raise ContractError("receipt reference has an invalid finite source duration")
                source_durations[(source_path, expected_digest)] = float(source_duration)
                _validate_probe_evidence(
                    source.get("probe"), float(source_duration), label="receipt reference"
                )
    source_policy = receipt.get("source_availability_policy")
    if known_sources and source_policy not in {"allow_unavailable", "require_available"}:
        raise ContractError("receipt must declare an explicit source availability policy")
    for source_path, expected_digest in sorted(known_sources):
        path = Path(source_path)
        try:
            metadata = path.lstat()
        except FileNotFoundError:
            if source_policy == "require_available":
                raise ContractError(f"receipt source is unavailable: {source_path}") from None
            continue
        if stat.S_ISLNK(metadata.st_mode) or not stat.S_ISREG(metadata.st_mode):
            raise ContractError(f"receipt source is not a safe regular file: {source_path}")
        try:
            actual_digest = _sha256(path)
        except FileNotFoundError:
            if source_policy == "require_available":
                raise ContractError(f"receipt source is unavailable: {source_path}") from None
            continue
        except OSError:
            raise ContractError(f"receipt source cannot be securely read: {source_path}") from None
        if actual_digest != expected_digest:
            raise ContractError(f"receipt source SHA-256 changed after generation: {source_path}")

    emitted = {
        path.relative_to(out_dir).as_posix()
        for path in out_dir.rglob("*")
        if path.is_file() and path.name != "receipt.json"
    }
    bound_paths: list[str] = []
    for entry in entries:

View on GitHub (pinned to 8321021c54)