trikona / geometry /kernel.py
disssid's picture
Assembly · Phase J2: expose asm in the kernel sandbox
2ce2625
Raw
History Blame Contribute Delete
4.76 kB
"""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"]