"""Sandboxed executor for model-written geometry generators (SPEC_OPTION_A.md §4.1). `run(code, entry, params)` executes a generator `build(p) -> (verts, faces)` in a **separate interpreter process** with a wall-clock timeout, an address-space / CPU rlimit, and an import allowlist (numpy, math, itertools). SECURITY NOTE: the restricted builtins + import allowlist are a *correctness* guard (keep generators small and honest), NOT a security boundary — restricted Python is leaky. The real isolation is the separate process + rlimits + timeout, and, in production, the Modal function's network isolation. Do not rely on the allowlist to contain hostile code on a networked host. """ import json import os import subprocess import sys import numpy as np class KernelError(Exception): """Raised when a generator fails to execute or returns invalid geometry.""" # The runner executes inside a fresh `python -c` process. It reads a JSON request # from stdin and prints exactly one JSON result line to stdout. _RUNNER = r''' import sys, json, math, resource import builtins as _b try: import numpy as np except Exception: print(json.dumps({"tag": "err", "msg": "numpy import failed"})); sys.exit(0) req = json.loads(sys.stdin.read()) code = req["code"]; entry = req.get("entry", "build"); params = req.get("params") or {} mem_mb = int(req.get("mem_mb", 512)); cpu = int(req.get("cpu", 4)) for lim, val in ((resource.RLIMIT_CPU, (cpu, cpu)), (resource.RLIMIT_AS, (mem_mb * 1024 * 1024, mem_mb * 1024 * 1024))): try: resource.setrlimit(lim, val) except Exception: pass # best-effort; macOS does not enforce RLIMIT_AS reliably ALLOWED = {"numpy", "math", "itertools"} def _imp(name, *a, **k): if name.split(".")[0] not in ALLOWED: raise ImportError("disallowed import: " + name) return _b.__import__(name, *a, **k) SAFE = {n: getattr(_b, n) for n in ( "range", "len", "min", "max", "abs", "sum", "enumerate", "zip", "map", "filter", "list", "dict", "tuple", "set", "frozenset", "float", "int", "bool", "str", "round", "sorted", "reversed", "any", "all", "pow", "divmod", "print", "isinstance", "slice", "complex", "repr", "ValueError", "TypeError", "IndexError", "Exception")} SAFE["__import__"] = _imp g = {"__builtins__": SAFE} # Inject the assembly helpers as `asm` (SPEC_ASSEMBLY.md §6) WITHOUT widening the # generator's import allowlist: the runner imports the trusted module here (real # builtins) and hands the generator only the safe callables. Best-effort. try: sys.path.insert(0, req.get("geomdir", "")) import assembly as _asmmod g["asm"] = _asmmod.namespace() except Exception: pass try: exec(code, g) fn = g.get(entry) if not callable(fn): raise ValueError("no callable %r" % entry) verts, faces = fn(params) V = np.asarray(verts, dtype=float).reshape(-1, 3) if not np.isfinite(V).all(): raise ValueError("non-finite vertices") n = len(V) F = [] for f in faces: i, j, k = int(f[0]), int(f[1]), int(f[2]); cid = str(f[3]) if not (0 <= i < n and 0 <= j < n and 0 <= k < n): raise ValueError("face index out of range") F.append([i, j, k, cid]) if not F: raise ValueError("no faces returned") print(json.dumps({"tag": "ok", "verts": V.tolist(), "faces": F})) except Exception as e: print(json.dumps({"tag": "err", "msg": repr(e)})) ''' def run(code, entry="build", params=None, timeout=None, mem_mb=512): """Execute `code`'s `entry(params)` in a sandbox. Return (verts ndarray, faces list). Raises KernelError on timeout, disallowed import, exception, or invalid output. """ if timeout is None: timeout = float(os.environ.get("TRIKONA_KERNEL_TIMEOUT", "3")) payload = json.dumps({ "code": code, "entry": entry, "params": params or {}, "geomdir": os.path.dirname(os.path.abspath(__file__)), # for `import assembly` "mem_mb": mem_mb, "cpu": int(timeout) + 1, }) try: proc = subprocess.run( [sys.executable, "-c", _RUNNER], input=payload, capture_output=True, text=True, timeout=timeout + 1.0, ) except subprocess.TimeoutExpired: raise KernelError("timeout") out = (proc.stdout or "").strip() if not out: tail = (proc.stderr or "").strip()[-200:] raise KernelError("no output: " + (tail or "process crashed")) try: res = json.loads(out.splitlines()[-1]) except Exception: raise KernelError("unparseable output") if res.get("tag") == "err": raise KernelError(res.get("msg", "unknown error")) return np.asarray(res["verts"], dtype=float), res["faces"]