Pedro
validate tool inputs with reusable pydantic types
c706455
Raw
History Blame Contribute Delete
1.87 kB
from __future__ import annotations
from pathlib import Path, PurePosixPath
def normalise_relative_path(path: str) -> PurePosixPath:
"""Normalise and validate a project-relative path."""
cleaned = path.strip().replace("\\", "/")
if not cleaned:
raise ValueError("Path cannot be empty.")
candidate = PurePosixPath(cleaned)
if candidate.is_absolute():
raise PermissionError(f"Absolute paths are not allowed: {path}")
if ".." in candidate.parts:
raise PermissionError(f"Parent-directory traversal is not allowed: {path}")
return candidate
def reject_repeated_project_prefix(
project_directory: Path,
path: str,
) -> None:
"""Reject paths such as sandbox/project-name/src/file.py."""
candidate = normalise_relative_path(path)
parts = candidate.parts
project_name = project_directory.name
if len(parts) >= 2 and parts[0].lower() == "sandbox" and parts[1] == project_name:
remaining_parts = parts[2:]
suggested_path = (
PurePosixPath(*remaining_parts).as_posix() if remaining_parts else "."
)
raise PermissionError(
"Do not include the sandbox or project-directory prefix. "
f"Use `{suggested_path}` instead of `{path}`."
)
def resolve_project_path(
project_directory: Path,
path: str,
) -> Path:
"""Resolve a project-relative path and ensure it stays inside the project."""
cleaned_path = path.strip()
reject_repeated_project_prefix(
project_directory,
cleaned_path,
)
relative_path = normalise_relative_path(cleaned_path)
root = project_directory.resolve()
resolved = (root / relative_path).resolve()
if not resolved.is_relative_to(root):
raise PermissionError(f"Path escapes the project directory: {path!r}")
return resolved