Implement configuration, runtime, frontend, and task governance closure
This commit is contained in:
104
web/worker/python-bridge.mjs
Normal file
104
web/worker/python-bridge.mjs
Normal file
@@ -0,0 +1,104 @@
|
||||
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 };
|
||||
Reference in New Issue
Block a user