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
- Replace the symlink/special file with a real regular file copy at the declared path
- Update the receipt's path to point at the actual regular file and regenerate its sha256
- 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
- Never symlink declared sources into a project; copy them
- Point receipt paths at regular files, never directories or sockets
- Check lstat mode of each source before generating a receipt
- Enable core.symlinks=false or equivalent in environments that convert files to links
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
- gate.variant_invalid
- output artifact must be a regular file
- output bundle contains a symlink
- output bundle root must not be a symlink
- output destination must not be a symlink
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)