feat: replace Aider with secure mini-SWE-agent worker
This commit is contained in:
@@ -1,84 +1,63 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path, PurePosixPath
|
||||
from pathlib import PurePosixPath
|
||||
|
||||
from .config import Settings
|
||||
from .git import ChangeSummary
|
||||
from .models import FailureReport
|
||||
from .git import GitWorkspace
|
||||
from .models import ChangeSummary
|
||||
|
||||
_ALWAYS_DENIED = (
|
||||
".env",
|
||||
"vendor/",
|
||||
"storage/",
|
||||
_PROTECTED_PARTS = {".git", "vendor", "node_modules", "storage"}
|
||||
_PROTECTED_PREFIXES = (
|
||||
".gitea/",
|
||||
"bootstrap/cache/",
|
||||
"docker/",
|
||||
"docker-compose",
|
||||
".gitea/",
|
||||
".github/",
|
||||
".aider",
|
||||
)
|
||||
|
||||
_DIRECT_MAIN_DENIED = (
|
||||
"database/migrations/",
|
||||
"app/Http/Middleware/",
|
||||
"app/Models/User.php",
|
||||
"app/Services/PermissionService.php",
|
||||
"app/Services/SQLValidatorService.php",
|
||||
"config/auth.php",
|
||||
"config/database.php",
|
||||
"routes/",
|
||||
)
|
||||
_PROTECTED_FILES = {
|
||||
"Dockerfile",
|
||||
"docker-compose.yml",
|
||||
"docker-compose.yaml",
|
||||
"compose.yml",
|
||||
"compose.yaml",
|
||||
}
|
||||
_CODE_SUFFIXES = {".php", ".js", ".ts", ".vue", ".css", ".py"}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PolicyResult:
|
||||
allowed: bool
|
||||
errors: tuple[str, ...]
|
||||
|
||||
|
||||
def evaluate_changes(
|
||||
def validate_changes(
|
||||
workspace: GitWorkspace,
|
||||
summary: ChangeSummary,
|
||||
settings: Settings,
|
||||
workspace: Path | None = None,
|
||||
) -> PolicyResult:
|
||||
errors: list[str] = []
|
||||
) -> None:
|
||||
if workspace.has_path(".git"):
|
||||
raise ValueError("The agent created forbidden Git metadata in the worktree")
|
||||
if not summary.files:
|
||||
errors.append("Agent produced no file changes")
|
||||
elif all(filename.startswith("tests/") for filename in summary.files):
|
||||
errors.append("Agent changed only tests; production behavior was not fixed")
|
||||
raise ValueError("The agent did not produce a change")
|
||||
if len(summary.files) > settings.max_changed_files:
|
||||
errors.append(f"Changed {len(summary.files)} files; limit is {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:
|
||||
errors.append(f"Changed {summary.changed_lines} lines; limit is {settings.max_changed_lines}")
|
||||
|
||||
for filename in summary.files:
|
||||
if PurePosixPath(filename).is_absolute() or ".." in PurePosixPath(filename).parts:
|
||||
errors.append(f"Invalid changed path: {filename}")
|
||||
continue
|
||||
if workspace and (workspace / filename).is_symlink():
|
||||
errors.append(f"Symbolic links are not allowed: {filename}")
|
||||
continue
|
||||
if any(filename == prefix or filename.startswith(prefix) for prefix in _ALWAYS_DENIED):
|
||||
errors.append(f"Denied path changed: {filename}")
|
||||
if settings.mode == "direct-main" and any(
|
||||
filename == prefix or filename.startswith(prefix) for prefix in _DIRECT_MAIN_DENIED
|
||||
):
|
||||
errors.append(f"Direct-main denied path changed: {filename}")
|
||||
|
||||
return PolicyResult(not errors, tuple(errors))
|
||||
|
||||
|
||||
def evaluate_report_scope(report: FailureReport, summary: ChangeSummary) -> PolicyResult:
|
||||
expected = f"{report.user_feedback or ''}\n{report.expected_behavior or ''}".lower()
|
||||
requires_cross_database_orchestration = (
|
||||
"cockpit_config" in expected and ("fk_" in expected or "erp" in expected)
|
||||
)
|
||||
has_application_change = any(filename.startswith("app/") for filename in summary.files)
|
||||
errors = []
|
||||
if requires_cross_database_orchestration and not has_application_change:
|
||||
errors.append(
|
||||
"Cross-database report changed no application code; inspect routing and implement staged orchestration"
|
||||
raise ValueError(
|
||||
f"Changed line limit exceeded "
|
||||
f"({summary.changed_lines} > {settings.max_changed_lines})"
|
||||
)
|
||||
|
||||
return PolicyResult(not errors, tuple(errors))
|
||||
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 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")
|
||||
|
||||
Reference in New Issue
Block a user