affaan-m/ECC · error · ValueError

output artifact must be a regular file

Error message

output artifact must be a regular file: {relative}

What it means

Before replacing an artifact, write_json stats the existing name (no symlink following) and refuses to overwrite anything that is not a regular file. This prevents clobbering directories, FIFOs, sockets, or symlinks placed at the artifact path.

Solutions

  1. Remove or replace the non-regular entry at the artifact path, then re-run
  2. Point the workflow at a fresh, trusted output directory
  3. If a symlink is intentional, copy the target's content into a real file at the artifact path instead

Example fix

# before: output/manifest.json is a symlink
# $ rm output/manifest.json
$ ls -l output/manifest.json   # verify regular file after rerun
# after: workflow writes a fresh regular file
Defensive patterns

Strategy: validation

Validate before calling

import os, stat
target = Path(output_root) / relative
if target.exists() or target.is_symlink():
    st = os.lstat(target)
    assert stat.S_ISREG(st.st_mode), f"{target} is not a regular file"

Type guard

def is_regular_file_or_absent(path: Path) -> bool:
    if not (path.exists() or path.is_symlink()):
        return True
    import os, stat
    return stat.S_ISREG(os.lstat(path).st_mode)

Try / catch

try:
    writer.write_json(relative, payload)
except ValueError:
    target = Path(output_root) / relative
    if target.is_symlink() or (target.exists() and not target.is_file()):
        target.unlink()
        writer.write_json(relative, payload)
    else:
        raise

Prevention

When it happens

Trigger: write_json targets a path where an existing entry is a directory, FIFO, device, or symlink; e.g. 'output/manifest.json' is a symlink planted by an attacker or a leftover directory.

Common situations: Attacker-controlled output directories with symlink preplants; a previous version of the tool created a directory at the artifact name; users manually linked outputs to shared locations.

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/19777bdaed6826ed. Report an issue: GitHub.

Appendix: source

Thrown at skills/taste-application/scripts/tasteforge/workflow.py:249

                opened.append(self._open_dir((directory,), create=True))
        finally:
            for descriptor in opened:
                os.close(descriptor)

    def write_json(self, relative: str, payload: Any) -> None:
        path = Path(relative)
        if path.is_absolute() or not path.name or any(part in {".", ".."} for part in path.parts):
            raise ValueError("artifact path must stay beneath output root")
        parent_fd = self._open_dir(tuple(path.parts[:-1]), create=False)
        temporary = f".{path.name}.tmp-{secrets.token_hex(8)}"
        descriptor = -1
        try:
            try:
                existing = os.stat(path.name, dir_fd=parent_fd, follow_symlinks=False)
            except FileNotFoundError:
                existing = None
            if existing is not None and not stat.S_ISREG(existing.st_mode):
                raise ValueError(f"output artifact must be a regular file: {relative}")
            data = json.dumps(payload, indent=2, sort_keys=True).encode("utf-8") + b"\n"
            descriptor = os.open(
                temporary,
                os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW,
                0o600,
                dir_fd=parent_fd,
            )
            view = memoryview(data)
            while view:
                written = os.write(descriptor, view)
                view = view[written:]
            os.fsync(descriptor)
            os.close(descriptor)
            descriptor = -1
            os.replace(temporary, path.name, src_dir_fd=parent_fd, dst_dir_fd=parent_fd)
            os.fsync(parent_fd)
            if relative not in self._written:
                self._written.append(relative)

View on GitHub (pinned to 8321021c54)