| from __future__ import annotations |
|
|
| import shutil |
| import tempfile |
| from datetime import UTC, datetime |
| from pathlib import Path |
| from typing import TYPE_CHECKING |
|
|
| from codeforge.sandbox.imports import scan_imports |
| from codeforge.sandbox.metric import composite_score |
| from codeforge.sandbox.models import ( |
| ImportReport, |
| ParsedResult, |
| SandboxResult, |
| ToolResult, |
| ) |
| from codeforge.sandbox.runner import run_tool |
| from codeforge.sandbox.tools import ( |
| DEFAULT_TOOLS, |
| argv_for, |
| is_available, |
| parse, |
| ) |
|
|
| if TYPE_CHECKING: |
| from collections.abc import Iterable |
|
|
|
|
| def run_sandbox( |
| *, |
| project_dir: Path | None = None, |
| files: dict[str, str] | None = None, |
| tools: Iterable[str] = DEFAULT_TOOLS, |
| timeout_per_tool: float = 60.0, |
| ) -> SandboxResult: |
| if (project_dir is None) == (files is None): |
| msg = "exactly one of project_dir / files must be set" |
| raise ValueError(msg) |
|
|
| tmp_root: Path | None = None |
| try: |
| if files is not None: |
| tmp_root = Path(tempfile.mkdtemp(prefix="codeforge_sandbox_")) |
| tmp_root_resolved = tmp_root.resolve() |
| for name, content in files.items(): |
| target = (tmp_root / name).resolve() |
| if not target.is_relative_to(tmp_root_resolved): |
| msg = f"path escapes sandbox root: {name!r}" |
| raise ValueError(msg) |
| target.parent.mkdir(parents=True, exist_ok=True) |
| target.write_text(content, encoding="utf-8") |
| project_dir = tmp_root |
|
|
| assert project_dir is not None |
| tool_list = tuple(tools) |
| tool_results: dict[str, ToolResult] = {} |
| parsed_results: dict[str, ParsedResult] = {} |
| imports_report: ImportReport | None = None |
|
|
| for name in tool_list: |
| if name == "imports": |
| imports_report = scan_imports(project_dir) |
| parsed_results[name] = ParsedResult( |
| ok=len(imports_report.unresolved) == 0, |
| count=len(imports_report.unresolved), |
| details={"unresolved": list(imports_report.unresolved)}, |
| ) |
| continue |
| if not is_available(name): |
| tool_results[name] = ToolResult( |
| name=name, argv=(name,), exit_code=-1, |
| stdout="", stderr="binary not found", |
| duration_ms=0, timed_out=False, |
| ) |
| parsed_results[name] = ParsedResult( |
| ok=False, count=0, details={"unavailable": True}, |
| ) |
| continue |
| argv = argv_for(name, project_dir) |
| tr = run_tool(name, argv, cwd=project_dir, timeout=timeout_per_tool) |
| tool_results[name] = tr |
| parsed_results[name] = parse(name, tr) |
|
|
| if imports_report is None: |
| imports_report = ImportReport(total=0, unresolved=(), by_file={}) |
|
|
| result = SandboxResult( |
| project_dir=str(project_dir), |
| tools_run=tool_list, |
| tool_results=tool_results, |
| parsed=parsed_results, |
| imports=imports_report, |
| composite_score=0.0, |
| generated_at=datetime.now(UTC).isoformat(timespec="seconds"), |
| ) |
| score = composite_score(result) |
| return result.model_copy(update={"composite_score": score}) |
| finally: |
| if tmp_root is not None and tmp_root.exists(): |
| shutil.rmtree(tmp_root, ignore_errors=True) |
|
|