/** * Pyodide sandbox runner — executes untrusted Prefab code in WASM. * * Protocol (JSON lines over stdin/stdout): * Request: {"id": "...", "code": "...", "data": {"key": "value"}} * Response: {"id": "...", "result": } or {"id": "...", "error": "..."} * * The process stays warm between requests. Prefab is pre-loaded * from source files mounted into Pyodide's virtual filesystem. * * Bootstrap needs filesystem reads for source/runtime files, writes for * Pyodide's downloaded wheel cache, and network access to its CDN. These * permissions are revoked before accepting generated code. --no-prompt * prevents permission requests from granting them again. The warm Python * interpreter and its imported modules remain shared across requests. */ import { loadPyodide } from "npm:pyodide@0.27.8"; import { readLines } from "https://deno.land/std@0.224.0/io/read_lines.ts"; import { walk } from "https://deno.land/std@0.224.0/fs/walk.ts"; // Suppress Pyodide's loadPackage logs from polluting stdout const realLog = console.log; console.log = (...args) => console.error("[pyodide]", ...args); // Generated Python output must never enter the JSON-lines response channel. // Suppress stderr too: its pipe is only drained during startup, so forwarding // arbitrary user output there could block the subprocess once the pipe fills. const pyodide = await loadPyodide({ stdout: () => {}, stderr: () => {}, // Keep the pre-0.27.7 event-loop behavior without requiring stack switching. enableRunUntilComplete: false, }); await pyodide.loadPackage(["pydantic"]); console.log = realLog; // Mount Prefab source into Pyodide's virtual filesystem. // The path is passed as the first CLI argument. const prefabSrc = Deno.args[0]; if (!prefabSrc) { console.error("Usage: runner.js "); Deno.exit(1); } const sitePackages = "/lib/python3.12/site-packages"; const createdDirs = new Set(); for await (const entry of walk(prefabSrc, { exts: [".py"] })) { if (!entry.isFile) continue; const relPath = entry.path.slice(prefabSrc.length).replaceAll("\\", "/"); const target = `${sitePackages}/prefab_ui${relPath}`; const dir = target.slice(0, target.lastIndexOf("/")); if (!createdDirs.has(dir)) { pyodide.FS.mkdirTree(dir); createdDirs.add(dir); } pyodide.FS.writeFile(target, await Deno.readTextFile(entry.path)); } // Verify Prefab loads await pyodide.runPythonAsync("import prefab_ui"); // ── Harness ───────────────────────────────────────────────────────── const HARNESS = ` import json as _json from prefab_ui.components.base import _component_stack from prefab_ui.components.base import Component as _C, ContainerComponent as _CC from prefab_ui.app import PrefabApp as _PA def _execute_prefab(_code, _data): # Reset component stack between executions _component_stack.set(None) # Fresh namespace with injected data _ns = dict(_data) exec(_code, _ns) # Find result: prefer PrefabApp, then root components _apps = [v for k, v in _ns.items() if not k.startswith('_') and isinstance(v, _PA)] _comps = [v for k, v in _ns.items() if not k.startswith('_') and isinstance(v, _C)] # Filter to roots — components not children of any container _all_children = set() for _c in _comps: if isinstance(_c, _CC): for _ch in _c.children: _all_children.add(id(_ch)) _roots = [_c for _c in _comps if id(_c) not in _all_children] _target = _apps[-1] if _apps else (_roots[-1] if _roots else None) if _target is None: raise ValueError("Code must assign a PrefabApp or Component to a variable") if isinstance(_target, _PA): return _target.to_json() return {"$prefab": {"version": "0.2"}, "view": _target.to_json()} `; await pyodide.runPythonAsync(HARNESS); // Bootstrap is complete: ordinary Prefab code uses the mounted virtual // filesystem and preloaded packages. Do not retain bootstrap I/O permissions // while executing generated code. for (const name of ["read", "write", "net"]) { await Deno.permissions.revoke({ name }); } console.error("pyodide:ready"); // ── Request loop ──────────────────────────────────────────────────── for await (const line of readLines(Deno.stdin)) { if (!line.trim()) continue; // Keep correlation metadata in JavaScript; generated Python receives only // its code and data. The parent rejects output from any other request. let requestId; try { const { id, code, data } = JSON.parse(line); requestId = id; const resultStr = await pyodide.runPythonAsync( `_json.dumps({"result": _execute_prefab(${JSON.stringify( code, )}, _json.loads(${JSON.stringify(JSON.stringify(data || {}))}))})`, ); realLog(JSON.stringify({ ...JSON.parse(resultStr), id: requestId })); } catch (err) { const msg = String(err.message || err); const lines = msg.split("\n").filter((l) => l.trim()); const short = lines[lines.length - 1] || msg; realLog(JSON.stringify({ id: requestId, error: short })); } }