validate_code
Check build123d code for syntax errors, blocked imports, and missing calls to prevent failed execution.
Instructions
Check build123d code for syntax errors, blocked imports/calls, and common omissions before executing. Returns {syntax, blocked, warnings, ok}. blocked items prevent execution; warnings are advisory (e.g. no build123d import in this snippet, no result/show() call). Use this before a long generated script to catch obvious problems without burning a session execute().
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| code | Yes |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
| result | Yes |
Implementation Reference
- Core handler that validates build123d code using AST inspection: checks syntax, blocked imports (against IMPORT_ALLOWLIST), blocked calls (against _BLOCKED_CALL_NAMES), dunder attribute access, and advisory warnings for missing build123d import or no 'result' assignment/show() call. Returns JSON string with 'syntax', 'blocked', 'warnings', and 'ok' fields.
def validate_code(code: str) -> str: # Syntax check try: tree = ast.parse(code) except SyntaxError as e: return json.dumps({ "syntax": f"SyntaxError at line {e.lineno}: {e.msg}", "blocked": [], "warnings": [], "ok": False, }, indent=2) blocked = [] warnings = [] # Security: blocked imports and calls for node in ast.walk(tree): if isinstance(node, ast.Import): for alias in node.names: root = alias.name.split(".")[0] if root not in IMPORT_ALLOWLIST: blocked.append(f"import '{alias.name}' is not allowed") elif isinstance(node, ast.ImportFrom): if node.module: root = node.module.split(".")[0] if root not in IMPORT_ALLOWLIST: blocked.append(f"import '{node.module}' is not allowed") elif isinstance(node, ast.Call): if isinstance(node.func, ast.Name) and node.func.id in _BLOCKED_CALL_NAMES: blocked.append(f"call to '{node.func.id}' is not allowed") elif isinstance(node, ast.Attribute): if node.attr.startswith("__") and node.attr.endswith("__"): blocked.append(f"dunder attribute access '{node.attr}' is not allowed") # Advisory: no build123d import in this snippet has_b3d_import = any( (isinstance(n, ast.ImportFrom) and n.module and n.module.startswith("build123d")) or (isinstance(n, ast.Import) and any(a.name.startswith("build123d") for a in n.names)) for n in ast.walk(tree) ) if not has_b3d_import: warnings.append("no build123d import in this snippet — ok if already imported in a prior execute()") # Advisory: no result variable or show() call has_output = any( (isinstance(n, ast.Assign) and any( isinstance(t, ast.Name) and t.id == "result" for t in n.targets )) or (isinstance(n, ast.Call) and isinstance(n.func, ast.Name) and n.func.id == "show") for n in ast.walk(tree) ) if not has_output: warnings.append("no 'result' assignment or show() call — the session won't capture a shape unless current_shape is set by other means") return json.dumps({ "syntax": "ok", "blocked": blocked, "warnings": warnings, "ok": len(blocked) == 0, }, indent=2) - Return JSON schema for validate_code output: fields 'syntax' (string), 'blocked' (list of strings), 'warnings' (list of strings), 'ok' (boolean).
return json.dumps({ "syntax": "ok", "blocked": blocked, "warnings": warnings, "ok": len(blocked) == 0, }, indent=2) - src/build123d_mcp/server.py:135-139 (registration)MCP tool registration for 'validate_code' using @mcp.tool() decorator. The server function delegates to the handler in tools/validate_code.py.
@mcp.tool() def validate_code(code: str) -> str: """Check build123d code for syntax errors, blocked imports/calls, and common omissions before executing. Returns {syntax, blocked, warnings, ok}. blocked items prevent execution; warnings are advisory (e.g. no build123d import in this snippet, no result/show() call). Use this before a long generated script to catch obvious problems without burning a session execute().""" from build123d_mcp.tools.validate_code import validate_code as _validate_code return _validate_code(code) - src/build123d_mcp/security.py:29-183 (helper)IMPORT_ALLOWLIST define which module roots are permitted for imports (build123d, math, numpy, json, etc.), used by validate_code's AST checking logic.
IMPORT_ALLOWLIST = frozenset({ # CAD libraries "build123d", "bd_warehouse", # Numeric / math "math", "numpy", "decimal", "fractions", "statistics", "numbers", "random", # Data structures / utilities "collections", "itertools", "functools", "copy", "operator", "struct", # Type system "typing", "abc", "dataclasses", "enum", # String / text "re", "string", "textwrap", "pprint", # Serialisation (in-memory only — no I/O) "json", "base64", "hashlib", # Misc stdlib "io", "warnings", "contextlib", }) # When True, import checks are skipped entirely. Set via --allow-all-imports. ALLOW_ALL_IMPORTS: bool = False # Builtins that are dangerous even without an import. _BLOCKED_BUILTINS = frozenset({ "eval", "exec", "compile", "open", "breakpoint", "input", # Introspection builtins that enable subclass-traversal sandbox escapes. "getattr", "vars", "dir", "hasattr", }) # Bare-name calls that are caught at the AST level (before exec runs). _BLOCKED_CALL_NAMES = frozenset({ "__import__", "eval", "exec", "compile", "open", "breakpoint", "input", # Introspection calls that can bypass dunder-attribute blocking via strings. "getattr", "vars", "dir", "hasattr", }) # --------------------------------------------------------------------------- # Layer 1: AST inspection # --------------------------------------------------------------------------- def check_ast(code: str) -> None: """Raise ValueError if code contains disallowed imports or dangerous calls. Catches the most common injection patterns before exec() is ever called. Syntax errors are left for exec() to report with better messages. """ try: tree = ast.parse(code) except SyntaxError: return if ALLOW_ALL_IMPORTS: # Still block dangerous calls even in unrestricted mode. for node in ast.walk(tree): if isinstance(node, ast.Call): if isinstance(node.func, ast.Name) and node.func.id in _BLOCKED_CALL_NAMES: raise ValueError(f"Call to '{node.func.id}' is not allowed.") return for node in ast.walk(tree): if isinstance(node, ast.Import): for alias in node.names: _check_module(alias.name) elif isinstance(node, ast.ImportFrom): if node.module: _check_module(node.module) elif isinstance(node, ast.Call): if isinstance(node.func, ast.Name) and node.func.id in _BLOCKED_CALL_NAMES: raise ValueError( f"Call to '{node.func.id}' is not allowed." ) elif isinstance(node, ast.Attribute): if node.attr.startswith("__") and node.attr.endswith("__"): raise ValueError( f"Access to dunder attribute '{node.attr}' is not allowed. " f"Use operators and language syntax instead of explicit dunder access." ) def _check_module(dotted_name: str) -> None: root = dotted_name.split(".")[0] if root not in IMPORT_ALLOWLIST: raise ValueError( f"Import of '{dotted_name}' is not allowed. " f"This blocks filesystem (os, pathlib, shutil), network (socket, urllib, " f"requests), and shell access (subprocess). " f"Permitted: {sorted(IMPORT_ALLOWLIST)}" ) # --------------------------------------------------------------------------- # Layer 2: Restricted builtins # --------------------------------------------------------------------------- def make_restricted_builtins() -> dict[str, Any]: """Return a __builtins__ dict with dangerous functions removed. open / eval / exec / compile are removed outright. __import__ is replaced with an allowlisted version so that 'from build123d import *' works but 'import os' is blocked at the namespace level even if AST inspection is somehow bypassed. """ import builtins safe = vars(builtins).copy() for name in _BLOCKED_BUILTINS: safe.pop(name, None) _original_import = safe["__import__"] if ALLOW_ALL_IMPORTS: safe["__import__"] = _original_import return safe def _safe_import(name: str, *args: Any, **kwargs: Any) -> Any: root = name.split(".")[0] if root not in IMPORT_ALLOWLIST: raise ImportError( f"Import of '{name}' is not allowed. " f"Permitted: {sorted(IMPORT_ALLOWLIST)}" ) return _original_import(name, *args, **kwargs) safe["__import__"] = _safe_import return safe # --------------------------------------------------------------------------- # Timeout exception (raised by SIGALRM in Session or propagated by WorkerSession) # --------------------------------------------------------------------------- class ExecutionTimeout(Exception): pass - src/build123d_mcp/security.py:79-83 (helper)_BLOCKED_CALL_NAMES list of bare-name function calls blocked at AST level (eval, exec, open, getattr, etc.), used by validate_code's security checks.
_BLOCKED_CALL_NAMES = frozenset({ "__import__", "eval", "exec", "compile", "open", "breakpoint", "input", # Introspection calls that can bypass dunder-attribute blocking via strings. "getattr", "vars", "dir", "hasattr", })