can1357/oh-my-pi · error · ValueError

Unsafe {scheme}:// path (absolute or traversal): {path}

Error message

Unsafe {scheme}:// path (absolute or traversal): {path}

What it means

_resolve_omp_path rejects scheme paths whose relative component is absolute (e.g. scheme:///abs/x) or contains '..' segments, raising ValueError 'Unsafe <scheme>:// path (absolute or traversal)'. This is a path-traversal guard: every resolved path must stay inside the configured root for that scheme.

Source

Thrown at packages/coding-agent/src/eval/py/prelude.py:112

            return Path(path)
        scheme = match.group(1).lower()
        try:
            roots = json.loads(os.environ.get("PI_EVAL_LOCAL_ROOTS") or "{}")
        except (ValueError, TypeError):
            roots = {}
        root = roots.get(scheme) if isinstance(roots, dict) else None
        if not root:
            raise ValueError(f"Protocol paths are not supported by this helper: {path}")
        relative = unquote(match.group(2).replace("\\", "/"))
        # Mirror the host `path.resolve`/`resolveLocalUrlToPath`: normalize and
        # make absolute WITHOUT realpath'ing symlinks (Path.resolve would turn
        # /tmp into /private/tmp and diverge from the read-side resolution).
        root_path = os.path.abspath(root)
        if relative == "":
            return Path(root_path)
        rel_path = Path(relative)
        if rel_path.is_absolute() or ".." in rel_path.parts:
            raise ValueError(f"Unsafe {scheme}:// path (absolute or traversal): {path}")
        resolved = os.path.abspath(os.path.join(root_path, relative))
        if resolved != root_path and not resolved.startswith(root_path + os.sep):
            raise ValueError(f"{scheme}:// path escapes its root: {path}")
        return Path(resolved)

    def read(path: str | Path, offset: int = 1, limit: int | None = None) -> str:
        """Read file or read-tool URI contents. offset/limit are 1-indexed lines."""
        if _should_delegate_read(path):
            if limit is not None and limit <= 0:
                return ""
            selector = _read_line_selector(offset, limit)
            tool_path = path if selector is None else f"{path}:{selector}"
            return _read_tool_text(tool_path)
        p = _resolve_omp_path(path)
        data = p.read_text(encoding="utf-8")
        lines = data.splitlines(keepends=True)
        if offset > 1 or limit is not None:
            start = max(0, offset - 1)

View on GitHub (pinned to 9690622007)

Solutions

  1. Rewrite the URI to a path relative to the scheme root with no leading '/' and no '..' segments.
  2. Copy the target file into the root directory configured for that scheme, then reference it relatively.
  3. If the file genuinely lives outside the root, access it via a plain local path if the harness permits, or extend PI_EVAL_LOCAL_ROOTS to mount the needed directory.

Example fix

# before
read("omp:///../outside/secret.txt")
# after
read("omp://artifacts/result.json")  # relative, no traversal
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import PurePosixPath
def is_safe_rel(rel): p = PurePosixPath(rel); return not p.is_absolute() and '..' not in p.parts

Type guard

def is_protocol_path(p): return '://' in str(p)

Try / catch

try:
    data = read(uri)
except ValueError as e:
    if 'Unsafe' in str(e) or 'traversal' in str(e):
        raise PermissionError(f'Refusing path outside session root: {uri}') from e
    raise

Prevention

When it happens

Trigger: read()/write() with a URI whose path after the scheme is absolute ('scheme:///etc/passwd') or climbs out with '..' ('scheme://../../secret').

Common situations: LLM-generated code constructing URIs with leading slashes or trying to reach files outside the session root; joining user-supplied relative paths that include '..'; echoing back absolute paths captured from host tool output into a scheme URI.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/8d7b2b93b6cbbcd6. Report an issue: GitHub.