#!/usr/bin/env python3 """Validate submitted Studio source against the imported SimpleCADAPI runtime. This is a server-side guard only. Agent-facing API documentation remains the original SimpleCADAPI skill and its API Markdown pages. """ from __future__ import annotations import argparse import ast import importlib import json from pathlib import Path from typing import Any import simplecadapi as scad STDLIB_MODULES = ("gear", "bearing", "chain", "fastener") # Make the package's public ``scad.std.`` paths available before AST # resolution. Model source may use any of these documented standard factories. for _module_name in STDLIB_MODULES: importlib.import_module(f"simplecadapi.std.{_module_name}") def resolve_simplecad_path(parts: list[str]) -> Any: value: Any = scad for part in parts: value = getattr(value, part) return value class SourceValidator(ast.NodeVisitor): def __init__(self) -> None: self.package_aliases: set[str] = set() self.imported_names: dict[str, tuple[str, list[str]]] = {} self.errors: list[dict[str, object]] = [] def error(self, node: ast.AST, message: str) -> None: self.errors.append({ "line": getattr(node, "lineno", None), "column": getattr(node, "col_offset", None), "message": message, }) def visit_Import(self, node: ast.Import) -> None: for alias in node.names: if alias.name == "simplecadapi": self.package_aliases.add(alias.asname or "simplecadapi") self.generic_visit(node) def visit_ImportFrom(self, node: ast.ImportFrom) -> None: module = node.module or "" if module == "simplecadapi" or module.startswith("simplecadapi."): module_parts = module.split(".")[1:] for alias in node.names: if alias.name == "*": self.error(node, "Wildcard imports from SimpleCADAPI are not allowed in submitted source.") continue try: resolve_simplecad_path(module_parts + [alias.name]) except AttributeError: self.error(alias, f"SimpleCADAPI import does not exist: {module}.{alias.name}") self.imported_names[alias.asname or alias.name] = (module, module_parts + [alias.name]) self.generic_visit(node) def attribute_path(self, node: ast.Attribute) -> tuple[str, list[str]] | None: parts: list[str] = [node.attr] current: ast.AST = node.value while isinstance(current, ast.Attribute): parts.append(current.attr) current = current.value if isinstance(current, ast.Name) and current.id in self.package_aliases: return current.id, list(reversed(parts)) return None def visit_Attribute(self, node: ast.Attribute) -> None: resolved = self.attribute_path(node) if resolved: _, parts = resolved try: resolve_simplecad_path(parts) except AttributeError: self.error(node, f"SimpleCADAPI attribute does not exist: scad.{'.'.join(parts)}") self.generic_visit(node) def visit_Call(self, node: ast.Call) -> None: if isinstance(node.func, ast.Name) and node.func.id in {"getattr", "setattr", "hasattr", "dir", "inspect"}: self.error(node, "Dynamic or introspective SDK access is not allowed in submitted SimpleCADAPI source.") self.generic_visit(node) def validate_source(source_path: Path) -> dict[str, object]: try: source = source_path.read_text(encoding="utf-8") tree = ast.parse(source, filename=str(source_path)) except (OSError, SyntaxError) as exc: return {"valid": False, "errors": [{"message": f"Cannot parse Python source: {exc}"}]} validator = SourceValidator() validator.visit(tree) if not validator.package_aliases: validator.errors.append({"message": "Submitted SimpleCADAPI source must import simplecadapi as scad."}) return {"valid": not validator.errors, "errors": validator.errors} def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--source", type=Path, required=True) args = parser.parse_args() print(json.dumps(validate_source(args.source), ensure_ascii=True)) if __name__ == "__main__": main()