abhigyanpatwari/GitNexus · error · SandboxError

sandbox_copy accepts only regular files and directories: {re

Error message

sandbox_copy accepts only regular files and directories: {relative}

What it means

Raised by _SnapshotBuilder.copy_descriptor when a declared sandbox_copy path resolves to a filesystem entry that is neither a regular file nor a directory (e.g. a FIFO, socket, character/block device, or other special file). The snapshot pipeline can only freeze regular files and directories because it reflinks or buffered-copies bytes and records a sha256; special files have no stable byte content to capture. This guard fires after the directory branch and just before _copy_file, so it is the final type gate on the descriptor that was opened with O_NOFOLLOW.

Source

Thrown at eval/workflow_bench/task_assets.py:413

            for name in names:
                child_relative = relative / name
                child_metadata = os.stat(name, dir_fd=descriptor, follow_symlinks=False)
                if stat.S_ISLNK(child_metadata.st_mode):
                    if not self.allow_symlinks:
                        raise SandboxError(f"sandbox_copy must not traverse a symlink: {child_relative}")
                    self._copy_symlink(descriptor, name, child_relative, child_metadata)
                    continue
                child = _open_child(descriptor, name, child_relative)
                try:
                    self.copy_descriptor(child, child_relative)
                finally:
                    os.close(child)
            after = os.fstat(descriptor)
            if _mutation_identity(before) != _mutation_identity(after):
                raise SandboxError(f"sandbox_copy directory changed while snapshotting: {relative}")
            return
        if not stat.S_ISREG(before.st_mode):
            raise SandboxError(f"sandbox_copy accepts only regular files and directories: {relative}")
        self._copy_file(descriptor, relative, before)

    def _record_directory(self, relative: PurePosixPath) -> None:
        self._ensure_parents(relative.parent)
        self._record(AssetManifestEntry(path=relative, kind="directory"))
        destination = self.destination / Path(*relative.parts)
        destination.mkdir(mode=0o700, exist_ok=True)

    def _copy_file(self, descriptor: int, relative: PurePosixPath, before: os.stat_result) -> None:
        self._ensure_parents(relative.parent)
        if self.budget.total_bytes + before.st_size > MAX_TASK_ASSET_BYTES:
            raise SandboxError("sandbox_copy exceeds the total byte limit")
        destination = self.destination / Path(*relative.parts)
        flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_CLOEXEC", 0)
        output = os.open(destination, flags, 0o600)
        digest = hashlib.sha256()
        copied = 0
        try:

View on GitHub (pinned to d540b00184)

Solutions

  1. Inspect the declared path with `find <path> -type p -o -type s -o -type b -o -type c` and remove or exclude the offending special files from the declaration.
  2. Narrow the sandbox_copy declaration to the specific subdirectories or files you need instead of a broad root that sweeps in sockets/pipes.
  3. If the special file is a legitimate runtime artifact, ensure it is created inside the arm clone at runtime, not captured in the immutable snapshot.
  4. Re-run the benchmark after cleaning the repo working tree (`git clean -fdx`) to eliminate stray special files left by prior tooling.

Example fix

// before (task definition)
{"sandbox_copy": ["repo/build"]}  // build/ contains a leftover FIFO

// after
{"sandbox_copy": ["repo/build/dist", "repo/build/config.json"]}
// or remove the FIFO: rm -f repo/build/.lock-fifo
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path, PurePosixPath
import stat, os

def validate_no_special_files(repo: Path, declarations: list[str]) -> None:
    for raw in declarations:
        root = repo / raw
        for current, dirs, files in os.walk(root, followlinks=False):
            for name in files:
                p = Path(current) / name
                mode = p.lstat().st_mode
                if not stat.S_ISREG(mode):
                    raise ValueError(f"non-regular file in sandbox_copy: {p} (mode {oct(stat.S_IFMT(mode))})")
            for name in dirs:
                p = Path(current) / name
                if not stat.S_ISDIR(p.lstat().st_mode):
                    raise ValueError(f"non-directory entry in sandbox_copy dir list: {p}")

# run before TaskAssetCache.prepare
validate_no_special_files(repo_path, task["sandbox_copy"])

Type guard

import stat
from pathlib import Path

def is_regular_or_dir(p: Path) -> bool:
    m = p.lstat().st_mode
    return stat.S_ISREG(m) or stat.S_ISDIR(m)

Try / catch

from eval.workflow_bench.propposer_sandbox import SandboxError

try:
    snapshot = cache.prepare(task, repo=repo, resolved_sha=sha)
except SandboxError as exc:
    if "accepts only regular files" in str(exc):
        # scan and clean the declared tree, then re-run
        ...
    raise

Prevention

When it happens

Trigger: A task declares sandbox_copy of a path that contains a Unix special file: `os.mkfifo`, `socket.socket(AF_UNIX).bind(...)`, or a device node under `/dev` bind-mounted into the repo. Also triggered when a regular file is replaced with a special file between the _open_child stat and the fstat in copy_descriptor (a TOCTOU swap), though that path is more commonly hit via the 'changed while snapshotting' guards.

Common situations: Accidentally declaring a sandbox_copy root that includes a build artifact directory containing named pipes or sockets (e.g. leftover `.npm/_cacache` locks, postgres test harness sockets, X11-style `/tmp/.X11-unix` binds). Declaring a path that a previous test run left a FIFO in. Container setups where the repo is overlaid with device nodes.

Related errors


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