abhigyanpatwari/GitNexus · critical · RuntimeError

generated artifact changed type while opening: {path}

Error message

generated artifact changed type while opening: {path}

What it means

Thrown by _bounded_regular_bytes as a TOCTOU guard: after the lstat accepted the path as a regular non-symlink file, the path is opened with O_RDONLY|O_NOFOLLOW and the descriptor is fstat'd. If the now-open fd is not a regular file, something swapped the path's target between the lstat and the open, and the read is aborted.

Source

Thrown at eval/workflow_bench/runner_artifacts.py:442

            ":(exclude)docs/plans",
            ":(exclude).claude/skills",
        ],
    )
    return parse_shortstat(output)


def _bounded_regular_bytes(path: Path, *, limit: int) -> bytes:
    """Read at most ``limit`` bytes without following a generated link."""

    metadata = path.lstat()
    if stat.S_ISLNK(metadata.st_mode) or not stat.S_ISREG(metadata.st_mode):
        raise RuntimeError(f"generated artifact is not a regular non-symlink file: {path}")
    nofollow = getattr(os, "O_NOFOLLOW", 0)
    descriptor = os.open(path, os.O_RDONLY | nofollow)
    try:
        opened = os.fstat(descriptor)
        if not stat.S_ISREG(opened.st_mode):
            raise RuntimeError(f"generated artifact changed type while opening: {path}")
        chunks: list[bytes] = []
        remaining = limit
        while remaining > 0:
            chunk = os.read(descriptor, min(64 * 1024, remaining))
            if not chunk:
                break
            chunks.append(chunk)
            remaining -= len(chunk)
        return b"".join(chunks)
    finally:
        os.close(descriptor)


def capture_patch(sandbox: SandboxSession, worktree: Path, orig_sha: str) -> bytes:
    """Stream a final patch inside the sandbox while retaining a bounded prefix."""

    artifact_dir = Path(tempfile.mkdtemp(prefix=".wfbench-artifact-", dir=worktree))
    artifact_dir.chmod(0o700)

View on GitHub (pinned to d540b00184)

Solutions

  1. Quiesce the worktree (agent exited, no concurrent writer) before _bounded_regular_bytes runs.
  2. Run on a local filesystem where the open reflects the lstat atomically enough.
  3. If the swap is a legitimate agent rewrite, ensure capture happens only after the agent has fully released the file.
  4. Re-run the arm; genuine TOCTOU swaps require an active concurrent writer.

Example fix

// before — capturing while agent still finalizing
patch = capture_patch(sandbox, worktree, orig_sha)  # agent rewrites mid-capture

// after — join agent, then capture a stable file
agent_proc.wait()
patch = capture_patch(sandbox, worktree, orig_sha)
Defensive patterns

Strategy: validation

Validate before calling

import os, stat
from pathlib import Path

def open_is_stable_type(path: Path) -> bool:
    nofollow = getattr(os, 'O_NOFOLLOW', 0)
    try:
        fd = os.open(path, os.O_RDONLY | nofollow)
    except OSError:
        return False
    try:
        return stat.S_ISREG(os.fstat(fd).st_mode)
    finally:
        os.close(fd)

Type guard

null

Try / catch

try:
    data = _bounded_regular_bytes(patch, limit=MAX_PATCH_BYTES)
except RuntimeError as e:
    if 'changed type while opening' in str(e):
        # TOCTOU type swap; quiesce the writer and retry
        raise
    raise

Prevention

When it happens

Trigger: os.fstat(descriptor) on the opened fd reports not S_ISREG. Between path.lstat() and os.open(...), the path was replaced (e.g., a regular file was unlinked and a special file or symlink target put in its place, then opened).

Common situations: A concurrent process races to swap the artifact type during capture; the agent's own finalize step rewrites the patch mid-read; a filesystem quirk where O_NOFOLLOW still opened something unexpected.

Related errors


AI-assisted analysis of abhigyanpatwari/GitNexus@d540b00184 (2026-08-12). Data as JSON: /api/errors/cb125e215296d8a5. Report an issue: GitHub.