abhigyanpatwari/GitNexus · error · SandboxError

evidence must be a regular non-symlink file: {value}

Error message

evidence must be a regular non-symlink file: {value}

What it means

Raised by _evidence_bytes when a Path evidence value is a symlink or not a regular file (stat.S_ISREG false). The contract forbids symlinks and special files because they could redirect the redaction pass at the real target or yield non-file bytes (FIFO/socket/device), defeating the byte cap and redaction guarantees.

Source

Thrown at eval/workflow_bench/proposer_sandbox.py:198

def redact_text(text: str, secrets: Sequence[str] = ()) -> str:
    for secret in secrets:
        if secret:
            text = text.replace(secret, "[REDACTED]")
    text = _TOKEN_PATTERNS[0].sub("[REDACTED]", text)
    text = _TOKEN_PATTERNS[1].sub("[REDACTED]", text)
    text = _TOKEN_PATTERNS[2].sub(r"\1[REDACTED]", text)
    return _TOKEN_PATTERNS[3].sub(r"\1[REDACTED]@", text)


def _evidence_bytes(value: Any, secrets: Sequence[str]) -> bytes:
    if isinstance(value, Path):
        try:
            mode = value.lstat().st_mode
        except OSError as exc:
            raise SandboxError(f"evidence path is unreadable: {value}: {exc}") from exc
        if value.is_symlink() or not stat.S_ISREG(mode):
            raise SandboxError(f"evidence must be a regular non-symlink file: {value}")
        if value.stat().st_size > MAX_EVIDENCE_FILE_BYTES:
            raise SandboxError(f"evidence exceeds the per-file limit: {value}")
        raw = value.read_bytes()
        return redact_text(raw.decode(errors="replace"), secrets).encode()
    if isinstance(value, bytes):
        raw = value
    elif isinstance(value, str):
        raw = value.encode()
    else:
        raw = (json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n").encode()
    return redact_text(raw.decode(errors="replace"), secrets).encode()


def stage_evidence_bundle(
    destination: Path,
    entries: Mapping[str, Any],
    *,
    secrets: Sequence[str] = (),

View on GitHub (pinned to d540b00184)

Solutions

  1. Replace the symlink with the real file: read the target and pass its bytes, or copy the target into a plain file with shutil.copyfile then stage that.
  2. If you meant to stage a directory, enumerate its regular files and stage each as its own simple-name entry.
  3. Reject special files upstream: assert value.is_file() and not value.is_symlink() before building the entries mapping.
  4. For symlinks you control, dereference explicitly with value.resolve(strict=True) and stage the resolved regular file.

Example fix

// before
entries = {"out": Path("/workspace/out")}  # /workspace/out -> /secret/log
// after
target = Path("/workspace/out").resolve(strict=True)
assert target.is_file() and not target.is_symlink()
entries = {"out": target}
Defensive patterns

Strategy: validation

Validate before calling

import stat
from pathlib import Path

def is_regular_nonsymlink(p: Path) -> bool:
    try:
        mode = p.lstat().st_mode
    except OSError:
        return False
    return stat.S_ISREG(mode) and not stat.S_ISLNK(mode)

assert all(is_regular_nonsymlink(v) for v in entries.values() if isinstance(v, Path))

Type guard

import stat
from pathlib import Path

def is_regular_non_symlink_file(value: object) -> bool:
    if not isinstance(value, Path):
        return False
    try:
        mode = value.lstat().st_mode
    except OSError:
        return False
    return stat.S_ISREG(mode) and not stat.S_ISLNK(mode)

Try / catch

try:
    stage_evidence_bundle(dest, entries, secrets=secrets)
except SandboxError as exc:
    if 'regular non-symlink' in str(exc):
        # resolve and replace the offending entry with its real target bytes
        bad = extract_path_from(exc)
        entries[bad.name] = bad.resolve(strict=True).read_bytes()
    raise

Prevention

When it happens

Trigger: stage_evidence_bundle is handed a Path that lstat's as a symlink, a directory, a named pipe, a unix socket, a block/char device, or any non-regular inode.

Common situations: Evidence 'file' is actually a symlink created by a test harness for convenience; a directory was passed where a single file was expected; /tmp evidence path collided with an OS FIFO; a socket file was left in the workspace by a dev server; the proposer wrote evidence through a symlink farm.

Related errors


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