from __future__ import annotations from pathlib import PurePosixPath from .config import Settings from .git import GitWorkspace from .models import ChangeSummary _PROTECTED_PARTS = {".git", "vendor", "node_modules", "storage"} _PROTECTED_PREFIXES = ( ".gitea/", "bootstrap/cache/", "docker/", ) _PROTECTED_FILES = { "Dockerfile", "docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml", } _CODE_SUFFIXES = {".php", ".js", ".ts", ".vue", ".css", ".py"} _TEMPORARY_SUFFIXES = {".bak", ".backup", ".orig", ".rej", ".swp", ".temp", ".tmp"} _TEMPORARY_NAMES = {"debug.php", "test_debug.php", "scratch.php"} def validate_changes( workspace: GitWorkspace, summary: ChangeSummary, settings: Settings, ) -> None: if workspace.has_path(".git"): raise ValueError("The agent created forbidden Git metadata in the worktree") if not summary.files: raise ValueError("The agent did not produce a change") if len(summary.files) > settings.max_changed_files: raise ValueError( f"Changed file limit exceeded " f"({len(summary.files)} > {settings.max_changed_files})" ) if summary.changed_lines > settings.max_changed_lines: raise ValueError( f"Changed line limit exceeded " f"({summary.changed_lines} > {settings.max_changed_lines})" ) code_changed = False for raw_name in summary.files: name = raw_name.replace("\\", "/") path = PurePosixPath(name) if name.startswith("/") or ".." in path.parts: raise ValueError(f"Unsafe changed path: {name}") if name.startswith(".env") or path.name.startswith(".env"): raise ValueError(f"Protected environment file changed: {name}") if path.parts and path.parts[0] in _PROTECTED_PARTS: raise ValueError(f"Protected path changed: {name}") if name in _PROTECTED_FILES or name.startswith(_PROTECTED_PREFIXES): raise ValueError(f"Protected deployment path changed: {name}") if path.suffix.lower() in _TEMPORARY_SUFFIXES or path.name.lower() in _TEMPORARY_NAMES: raise ValueError(f"Temporary artifact changed: {name}") if workspace.has_symlink(name): raise ValueError(f"Symbolic links are not allowed in agent changes: {name}") if path.suffix in _CODE_SUFFIXES and not name.startswith(("tests/", "docs/")): code_changed = True if not code_changed: raise ValueError("The agent must change at least one application source file")