abhigyanpatwari/GitNexus · error · SandboxError

{label} must be a repository-relative path: {relative!r}

Error message

{label} must be a repository-relative path: {relative!r}

What it means

Raised by _safe_repo_source when the relative path is not a valid repository-relative path: candidate.is_absolute() is true, '..' appears in candidate.parts, or candidate.parts is empty. This stops absolute paths, parent-traversal, and empty inputs before any filesystem access, preventing escape from the repository root.

Source

Thrown at eval/workflow_bench/proposer_sandbox.py:564

    try:
        mode = lexical.lstat().st_mode
    except OSError as exc:
        raise SandboxError(f"{label} must be a real directory: {lexical}: {exc}") from exc
    if stat.S_ISLNK(mode) or not stat.S_ISDIR(mode):
        raise SandboxError(f"{label} must be a real directory: {lexical}")
    try:
        resolved = lexical.resolve(strict=True)
    except OSError as exc:
        raise SandboxError(f"{label} must be a real directory: {lexical}: {exc}") from exc
    if resolved != lexical:
        raise SandboxError(f"{label} must not traverse symlinks: {lexical}")
    return lexical


def _safe_repo_source(repo: Path, relative: str, *, label: str) -> tuple[Path, Path]:
    candidate = PurePosixPath(relative)
    if candidate.is_absolute() or ".." in candidate.parts or not candidate.parts:
        raise SandboxError(f"{label} must be a repository-relative path: {relative!r}")
    lexical = repo / Path(*candidate.parts)
    resolved = lexical.resolve()
    try:
        resolved.relative_to(repo)
    except ValueError as exc:
        raise SandboxError(f"{label} escapes its allowed repository root: {relative}") from exc
    if not resolved.exists():
        raise SandboxError(f"{label} does not exist: {relative}")
    return lexical, resolved


def _prepare_clone_target(
    clone: Path,
    relative: PurePosixPath,
    *,
    directory: bool | None,
    label: str,
) -> Path:

View on GitHub (pinned to d540b00184)

Solutions

  1. Pass a true relative path with no '..' segments: derive it via path.relative_to(repo).
  2. Validate before calling: assert not PurePosixPath(rel).is_absolute() and '..' not in PurePosixPath(rel).parts and PurePosixPath(rel).parts.
  3. Reject empty input upstream with a clear config error.
  4. If you genuinely need a parent path, change the design — the boundary forbids it by intent.

Example fix

// before
src, resolved = _safe_repo_source(repo, str(some_abs_path), label='task asset')
// after
rel = PurePosixPath(some_abs_path).relative_to(repo)
src, resolved = _safe_repo_source(repo, str(rel), label='task asset')
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import PurePosixPath

def is_repo_relative(rel: str) -> bool:
    p = PurePosixPath(rel)
    return (not p.is_absolute()
            and '..' not in p.parts
            and len(p.parts) > 0
            and p.name not in {'', '.', '..'})

assert is_repo_relative(relative)

Type guard

from pathlib import PurePosixPath

def is_safe_repo_relative(value: object) -> bool:
    if not isinstance(value, str):
        return False
    p = PurePosixPath(value)
    return (not p.is_absolute()
            and '..' not in p.parts
            and len(p.parts) > 0
            and p.name not in {'', '.', '..'})

Try / catch

try:
    src, resolved = _safe_repo_source(repo, relative, label=label)
except SandboxError as exc:
    if 'repository-relative path' in str(exc):
        rel = PurePosixPath(relative).relative_to(repo) if PurePosixPath(relative).is_absolute() else PurePosixPath(relative)
        src, resolved = _safe_repo_source(repo, str(rel), label=label)
    raise

Prevention

When it happens

Trigger: Calling _safe_repo_source with relative='/' (absolute), '/etc/passwd', '../outside', 'a/../../b' (after PurePosixPath normalizes parts), or '' (empty -> no parts).

Common situations: Caller used str(path) of an absolute Path as the relative argument; user-supplied or config-supplied path string contained '..'; an empty string passed from an unset config key; a path was normalized externally with PurePosixPath and the '..' collapsed oddly.

Related errors


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