Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| import ast | |
| from dataclasses import dataclass | |
| FORBIDDEN_CALL_NAMES = frozenset({"exec", "eval", "__import__", "compile"}) | |
| ALLOWED_IMPORT_ROOTS = frozenset( | |
| { | |
| "__future__", | |
| "manim", | |
| "primitives", | |
| "typing", | |
| "math", | |
| "numpy", | |
| }, | |
| ) | |
| class SandboxValidationError(ValueError): | |
| """Raised when generated Manim code fails static safety checks.""" | |
| class SandboxLimits: | |
| max_bytes: int | |
| def _import_root(name: str) -> str: | |
| return name.split(".", 1)[0] | |
| def _validate_imports(tree: ast.Module) -> None: | |
| for node in ast.walk(tree): | |
| if isinstance(node, ast.Import): | |
| for alias in node.names: | |
| root = _import_root(alias.name) | |
| if root not in ALLOWED_IMPORT_ROOTS: | |
| msg = f"Disallowed import root: {root!r}" | |
| raise SandboxValidationError(msg) | |
| if isinstance(node, ast.ImportFrom): | |
| if node.level and node.level > 0: | |
| msg = "Relative imports are not allowed in generated code" | |
| raise SandboxValidationError(msg) | |
| if node.module is None: | |
| msg = "Wildcard or ambiguous imports are not allowed" | |
| raise SandboxValidationError(msg) | |
| root = _import_root(node.module) | |
| if root not in ALLOWED_IMPORT_ROOTS: | |
| msg = f"Disallowed import-from module root: {root!r}" | |
| raise SandboxValidationError(msg) | |
| if isinstance(node, ast.Call): | |
| func = node.func | |
| if isinstance(func, ast.Name) and func.id in FORBIDDEN_CALL_NAMES: | |
| msg = f"Disallowed call: {func.id}()" | |
| raise SandboxValidationError(msg) | |
| def _validate_generated_scene_class(tree: ast.Module) -> None: | |
| for node in tree.body: | |
| if isinstance(node, ast.ClassDef) and node.name == "GeneratedScene": | |
| return | |
| msg = "Generated code must define class GeneratedScene" | |
| raise SandboxValidationError(msg) | |
| def validate_manim_code(source: str, *, limits: SandboxLimits) -> None: | |
| """Static checks: size, syntax, import policy, required class name.""" | |
| data = source.encode("utf-8") | |
| if len(data) > limits.max_bytes: | |
| msg = f"Code exceeds max size ({limits.max_bytes} bytes)" | |
| raise SandboxValidationError(msg) | |
| try: | |
| tree = ast.parse(source) | |
| except SyntaxError as exc: | |
| msg = f"Invalid Python syntax: {exc.msg} at line {exc.lineno}, col {exc.offset}" | |
| raise SandboxValidationError(msg) from exc | |
| if not isinstance(tree, ast.Module): | |
| msg = "Expected a module" | |
| raise SandboxValidationError(msg) | |
| _validate_imports(tree) | |
| _validate_generated_scene_class(tree) | |
| def static_check_split(source: str, *, limits: SandboxLimits) -> tuple[bool, bool, str | None]: | |
| """Return (syntax_ok, policy_ok, error_code). | |
| ``syntax_ok`` means Python parses. ``policy_ok`` adds import rules + GeneratedScene class. | |
| """ | |
| data = source.encode("utf-8") | |
| if len(data) > limits.max_bytes: | |
| return False, False, "size_exceeded" | |
| try: | |
| tree = ast.parse(source) | |
| except SyntaxError: | |
| return False, False, "syntax_error" | |
| if not isinstance(tree, ast.Module): | |
| return False, False, "syntax_error" | |
| try: | |
| _validate_imports(tree) | |
| _validate_generated_scene_class(tree) | |
| except SandboxValidationError: | |
| return True, False, "policy_error" | |
| return True, True, None | |