feat: replace Aider with secure mini-SWE-agent worker
Some checks failed
CI / test (push) Has been cancelled
CI / test (pull_request) Has been cancelled

This commit is contained in:
Christian Meyer
2026-08-31 02:46:58 +02:00
parent 582db3a654
commit 25ed6a8439
30 changed files with 4215 additions and 2148 deletions

View File

@@ -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")