Files
linuxcncWebWASM/web/worker/python-bridge.mjs

105 lines
5.2 KiB
JavaScript

import manifest from "../../python-runtime/manifest.json" with { type: "json" };
import { fnv1a64 } from "./replay.mjs";
export const PYTHON_BRIDGE_SCHEMA = "python-worker-bridge.v1";
function clone(value) { return structuredClone(value); }
function finiteJson(value) {
if (value === null || typeof value !== "object") return true;
if (typeof value === "number") return Number.isFinite(value);
return Array.isArray(value) ? value.every(finiteJson) : Object.values(value).every(finiteJson);
}
export class PythonBridge {
constructor({ loadPyodide = globalThis.loadPyodide, maxSourceBytes = 65536, maxCalls = 256 } = {}) {
this.loadPyodide = loadPyodide;
this.maxSourceBytes = maxSourceBytes;
this.maxCalls = maxCalls;
this.pyodide = null;
this.initialized = false;
this.state = { imports: [], calls: [], errors: [], capabilities: [], tick: 0 };
}
capabilities() {
return {
schema: PYTHON_BRIDGE_SCHEMA,
runtime: manifest.runtime,
version: manifest.version,
python_version: manifest.python_version,
abi_version: manifest.abi_version,
platform: manifest.platform,
worker_only: manifest.worker_only,
virtual_modules: clone(manifest.virtual_modules),
denied_capabilities: [...manifest.denied_capabilities],
initialized: this.initialized,
};
}
async initialize() {
if (this.initialized) return { status: "verified", capabilities: this.capabilities() };
if (typeof this.loadPyodide !== "function") return this.fail("python_runtime_unavailable", "python.runtime_unavailable");
try {
this.pyodide = await this.loadPyodide({ indexURL: manifest.index_url });
this.initialized = true;
return { status: "verified", capabilities: this.capabilities() };
} catch (error) {
return this.fail("python_runtime_init", String(error?.message ?? error));
}
}
importModule(module) {
const symbols = manifest.virtual_modules[module];
if (!symbols) return this.fail("python_unsupported_module", "python.unsupported_module", { module });
if (!this.state.imports.includes(module)) this.state.imports.push(module);
return { status: "verified", module, symbols: [...symbols], state_hash: this.hash() };
}
checkCapability(capability) {
if (manifest.denied_capabilities.includes(capability)) return this.fail("real_hardware_excluded", "python.host_capability_denied", { capability, status: "REAL-EXCLUDED" });
if (typeof capability !== "string" || capability.length === 0) return this.fail("python_capability_invalid", "python.capability_invalid");
if (!this.state.capabilities.includes(capability)) this.state.capabilities.push(capability);
return { status: "verified", capability, state_hash: this.hash() };
}
invoke(module, symbol, args = []) {
const symbols = manifest.virtual_modules[module];
if (!symbols || !symbols.includes(symbol)) return this.fail("python_symbol_unsupported", "python.symbol_unsupported", { module, symbol });
if (!Array.isArray(args) || !finiteJson(args)) return this.fail("python_argument_invalid", "python.argument_invalid");
if (this.state.calls.length >= this.maxCalls) return this.fail("python_call_limit", "python.call_limit");
if (!this.state.imports.includes(module)) this.state.imports.push(module);
const call = { module, symbol, args: clone(args), tick: this.state.tick };
this.state.calls.push(call);
return { status: "verified", value: { module, symbol, args: clone(args) }, state_hash: this.hash() };
}
async execute(source) {
if (typeof source !== "string" || new TextEncoder().encode(source).length > this.maxSourceBytes) return this.fail("python_source_limit", "python.source_limit");
const denied = manifest.denied_capabilities.find((capability) => new RegExp(`(?:^|[^A-Za-z0-9_])${capability.replaceAll(".", "\\.")}(?:$|[^A-Za-z0-9_])`).test(source));
if (denied) return this.fail("real_hardware_excluded", "python.host_capability_denied", { capability: denied, status: "REAL-EXCLUDED" });
if (!this.pyodide || typeof this.pyodide.runPythonAsync !== "function") return this.fail("python_runtime_unavailable", "python.runtime_unavailable");
try {
const value = await this.pyodide.runPythonAsync(source);
return { status: "verified", value: value === undefined ? null : value, state_hash: this.hash() };
} catch (error) {
return this.fail("python_exception", "python.exception", { detail: String(error?.message ?? error) });
}
}
fail(code, message_key, extra = {}) {
this.state.errors.push({ code, tick: this.state.tick });
return { status: extra.status ?? "blocked", code, message_key, retryable: false, ...extra, state_hash: this.hash() };
}
setTick(tick) { if (Number.isSafeInteger(tick) && tick >= 0) this.state.tick = tick; }
exportState() { return { schema: PYTHON_BRIDGE_SCHEMA, initialized: this.initialized, state: clone(this.state) }; }
restoreState(value) {
if (!value || value.schema !== PYTHON_BRIDGE_SCHEMA || !value.state) return this.fail("python_state_invalid", "python.state_invalid");
this.initialized = Boolean(value.initialized);
this.state = clone(value.state);
return { status: "verified", state_hash: this.hash() };
}
hash() { return fnv1a64(this.state); }
}
export { manifest };