manim_mcp / validators.py
euler314's picture
Upload 19 files
53dfbe8 verified
Raw
History Blame Contribute Delete
8.49 kB
"""
Code validation and security checking utilities
"""
import ast
from typing import Tuple, List, Optional
from config import ALLOWED_IMPORTS, BLOCKED_IMPORTS
def validate_python_syntax(code: str) -> Tuple[bool, str]:
"""
Validate Python code syntax using AST parsing
Args:
code: Python code string to validate
Returns:
Tuple of (is_valid, error_message)
- If valid: (True, "OK")
- If invalid: (False, "SyntaxError: ...")
"""
try:
ast.parse(code)
return True, "OK"
except SyntaxError as e:
error_msg = f"SyntaxError on line {e.lineno}: {e.msg}"
if e.text:
error_msg += f"\n {e.text.strip()}"
return False, error_msg
except Exception as e:
return False, f"Parse error: {str(e)}"
def check_for_dangerous_imports(code: str) -> Tuple[bool, str]:
"""
Check for potentially dangerous imports that could pose security risks
Args:
code: Python code string to analyze
Returns:
Tuple of (is_safe, error_message)
- If safe: (True, "OK")
- If unsafe: (False, "Blocked import: ...")
"""
try:
tree = ast.parse(code)
except SyntaxError:
# Syntax errors are handled by validate_python_syntax
return True, "OK"
for node in ast.walk(tree):
# Check regular imports: import os, import sys
if isinstance(node, ast.Import):
for alias in node.names:
module_name = alias.name.split('.')[0]
if module_name in BLOCKED_IMPORTS:
return False, f"Blocked import: '{alias.name}' - This module is restricted for security reasons"
# Optionally enforce whitelist (commented out for flexibility)
# if module_name not in ALLOWED_IMPORTS:
# return False, f"Unauthorized import: '{alias.name}' - Only {ALLOWED_IMPORTS} are allowed"
# Check from imports: from os import system
elif isinstance(node, ast.ImportFrom):
if node.module:
module_name = node.module.split('.')[0]
if module_name in BLOCKED_IMPORTS:
return False, f"Blocked import: 'from {node.module}' - This module is restricted for security reasons"
# Check for dangerous built-in functions
elif isinstance(node, ast.Name):
if node.id in BLOCKED_IMPORTS:
return False, f"Blocked function: '{node.id}' - This function is restricted for security reasons"
return True, "OK"
def extract_scene_classes(code: str) -> List[str]:
"""
Extract all Scene class names from Manim code using AST
Args:
code: Python code string containing Manim scenes
Returns:
List of Scene class names found in the code
"""
try:
tree = ast.parse(code)
except SyntaxError:
return []
scene_classes = []
for node in ast.walk(tree):
if isinstance(node, ast.ClassDef):
# Check if class inherits from Scene or any Scene-like class
for base in node.bases:
# Handle simple inheritance: class MyScene(Scene)
if isinstance(base, ast.Name):
if 'Scene' in base.id:
scene_classes.append(node.name)
break
# Handle module.Class inheritance: class MyScene(manim.Scene)
elif isinstance(base, ast.Attribute):
if 'Scene' in base.attr:
scene_classes.append(node.name)
break
return scene_classes
def validate_scene_exists(code: str, scene_name: str) -> Tuple[bool, str]:
"""
Verify that a specific scene class exists in the code
Args:
code: Python code string
scene_name: Name of the Scene class to verify
Returns:
Tuple of (exists, error_message)
- If exists: (True, "OK")
- If not found: (False, "Scene 'X' not found...")
"""
scene_classes = extract_scene_classes(code)
if scene_name in scene_classes:
return True, "OK"
else:
available = ", ".join(scene_classes) if scene_classes else "none"
return False, f"Scene '{scene_name}' not found in code. Available scenes: {available}"
def auto_detect_scene(code: str) -> Optional[str]:
"""
Automatically detect the scene to render when user doesn't specify one
Args:
code: Python code string
Returns:
Scene class name if exactly one is found, None otherwise
"""
scene_classes = extract_scene_classes(code)
if len(scene_classes) == 1:
return scene_classes[0]
else:
return None
def check_for_animation_commands(code: str) -> Tuple[bool, str]:
"""
Check if code contains animation commands (self.play() or self.wait())
Scenes without these will not generate video frames
Args:
code: Python code to check
Returns:
Tuple of (has_animations, error_message)
"""
has_play = 'self.play(' in code
has_wait = 'self.wait(' in code
if not (has_play or has_wait):
return False, (
"ERROR: Your scene must include 'self.play()' or 'self.wait()' to generate video frames. "
"Using only 'self.add()' will not create an animation. "
"Example: self.play(Create(circle)) or self.wait(1)"
)
return True, "OK"
def check_for_latex_usage(code: str) -> Tuple[bool, str]:
"""
Check if code uses LaTeX objects which may cause rendering issues
Args:
code: Python code to check
Returns:
Tuple of (has_latex, warning_message)
"""
latex_classes = ['MathTex', 'Tex', 'TexTemplate', 'Text']
found_latex = []
for latex_class in latex_classes:
if latex_class in code:
found_latex.append(latex_class)
if found_latex:
warning = (
f"Warning: Your code uses {', '.join(found_latex)} which requires LaTeX. "
f"This may cause timeouts. Consider using geometric shapes instead (Circle, Square, Line, etc.)."
)
return True, warning
return False, "OK"
def validate_code_full(code: str, scene_name: Optional[str] = None) -> Tuple[bool, str, Optional[str]]:
"""
Perform full code validation including syntax, security, and scene checks
Args:
code: Python code to validate
scene_name: Optional specific scene name to validate
Returns:
Tuple of (is_valid, error_message, detected_scene_name)
- is_valid: True if all checks pass
- error_message: Error description if validation fails, "OK" otherwise
- detected_scene_name: Auto-detected scene name (if scene_name was None)
"""
# Step 1: Check syntax
is_valid, error_msg = validate_python_syntax(code)
if not is_valid:
return False, error_msg, None
# Step 2: Check for dangerous imports
is_safe, error_msg = check_for_dangerous_imports(code)
if not is_safe:
return False, error_msg, None
# Step 2.5: Check for animation commands (REQUIRED)
has_animations, anim_error = check_for_animation_commands(code)
if not has_animations:
return False, anim_error, None
# Step 2.6: Check for LaTeX usage (warning only)
has_latex, latex_warning = check_for_latex_usage(code)
if has_latex:
# Log warning but don't fail
import logging
logger = logging.getLogger("manim_studio.validators")
logger.warning(latex_warning)
# Step 3: Check scenes
scene_classes = extract_scene_classes(code)
if not scene_classes:
return False, "No Scene classes found in code. Please define at least one class inheriting from manim.Scene", None
# Step 4: Validate or auto-detect scene
if scene_name:
# User specified a scene - verify it exists
is_valid, error_msg = validate_scene_exists(code, scene_name)
if not is_valid:
return False, error_msg, None
return True, "OK", scene_name
else:
# Auto-detect scene
if len(scene_classes) == 1:
return True, "OK", scene_classes[0]
else:
# Multiple scenes found, user must specify
available = ", ".join(scene_classes)
return False, f"Multiple Scene classes found: {available}. Please specify scene_name in your request.", None