import { fnv1a64 } from "./replay.mjs"; export const CALLBACK_SCHEMA = "python-callbacks.v1"; const MAX_CALLBACKS = 256; const MAX_SOURCE_BYTES = 65536; function clone(value) { return structuredClone(value); } function validName(value) { return typeof value === "string" && /^[A-Za-z_][A-Za-z0-9_.-]{0,63}$/.test(value); } function finiteJson(value) { if (typeof value === "number") return Number.isFinite(value); if (Array.isArray(value)) return value.every(finiteJson); if (value && typeof value === "object") return Object.values(value).every(finiteJson); return true; } function typedValue(type, value = 0) { if (type === "bit") { if (![0, 1, false, true].includes(value)) throw new Error("vcp_bit_range"); return value ? 1 : 0; } if (type === "s32") { if (!Number.isSafeInteger(value) || value < -2147483648 || value > 2147483647) throw new Error("vcp_s32_range"); return value; } if (type === "u32") { if (!Number.isSafeInteger(value) || value < 0 || value > 4294967295) throw new Error("vcp_u32_range"); return value; } if (type === "float" && typeof value === "number" && Number.isFinite(value)) return value; throw new Error("vcp_value_type"); } export class PythonCallbacks { constructor({ maxCallbacks = MAX_CALLBACKS } = {}) { this.maxCallbacks = maxCallbacks; this.reset(); } reset() { this.state = { schema: CALLBACK_SCHEMA, tick: 0, remaps: {}, vcp: {}, filters: {}, calls: [], errors: [], reentrant: false }; this.handlers = new Map(); return this.export(); } export() { const value = clone(this.state); return { ...value, state_hash: fnv1a64(value) }; } hash() { return this.export().state_hash; } fail(code, detail = null) { this.state.errors.push({ code, tick: this.state.tick }); return { status: "blocked", code, ...(detail ? { detail } : {}), state_hash: this.hash() }; } setTick(tick) { if (!Number.isSafeInteger(tick) || tick < this.state.tick) return this.fail("callback_clock_regression"); this.state.tick = tick; return { status: "verified", state_hash: this.hash() }; } registerRemap(name, descriptor = {}, handler = null) { const { prolog = null, epilog = null, ngc = null, python = null, modalgroup = null } = descriptor ?? {}; if (!validName(name) || (prolog !== null && !validName(prolog)) || (epilog !== null && !validName(epilog)) || (ngc !== null && !validName(ngc)) || (python !== null && !validName(python)) || (modalgroup !== null && (!Number.isSafeInteger(modalgroup) || modalgroup < 0))) return this.fail("remap_descriptor_invalid"); if (this.state.remaps[name]) return this.fail("remap_duplicate"); this.state.remaps[name] = { name, prolog, epilog, ngc, python, modalgroup, calls: 0, failures: 0 }; if (typeof handler === "function") this.handlers.set(`remap:${name}`, handler); else if (handler && typeof handler === "object") for (const phase of ["prolog", "epilog"]) if (typeof handler[phase] === "function") this.handlers.set(`remap:${name}:${phase}`, handler[phase]); return { status: "verified", descriptor: clone(this.state.remaps[name]), state_hash: this.hash() }; } registerVcp(name, pins = [], handler = null) { if (!validName(name) || !Array.isArray(pins) || pins.length > 256 || this.state.vcp[name] || pins.some((pin) => !validName(pin?.name) || !["bit", "s32", "u32", "float"].includes(pin?.type))) return this.fail(this.state.vcp[name] ? "vcp_duplicate" : "vcp_descriptor_invalid"); try { this.state.vcp[name] = { name, pins: pins.map((pin) => ({ name: pin.name, type: pin.type, value: typedValue(pin.type, pin.value ?? 0) })), calls: 0, failures: 0 }; } catch (error) { return this.fail("vcp_descriptor_invalid", String(error.message)); } if (typeof handler === "function") this.handlers.set(`vcp:${name}`, handler); return { status: "verified", descriptor: clone(this.state.vcp[name]), state_hash: this.hash() }; } registerFilter(name, handler = null) { if (!validName(name)) return this.fail("filter_name_invalid"); if (this.state.filters[name]) return this.fail("filter_duplicate"); this.state.filters[name] = { name, calls: 0, failures: 0 }; if (typeof handler === "function") this.handlers.set(`filter:${name}`, handler); return { status: "verified", descriptor: clone(this.state.filters[name]), state_hash: this.hash() }; } invoke(kind, name, phase = "prolog", args = []) { if (this.state.calls.length >= this.maxCallbacks) return this.fail("callback_budget"); if (!Array.isArray(args) || !finiteJson(args)) return this.fail("callback_arguments_invalid"); if (kind === "remap" && !["prolog", "epilog"].includes(phase)) return this.fail("remap_phase_invalid"); const key = `${kind}:${name}`; const descriptor = kind === "remap" ? this.state.remaps[name] : kind === "vcp" ? this.state.vcp[name] : this.state.filters[name]; if (!descriptor) return this.fail("callback_missing"); if (kind === "remap" && descriptor[phase] === null) return this.fail("remap_phase_unsupported"); if (this.state.reentrant) return this.fail("callback_reentrant"); const handler = this.handlers.get(kind === "remap" ? `${key}:${phase}` : key) ?? this.handlers.get(key); this.state.reentrant = true; try { const value = handler ? handler({ phase, args: clone(args), tick: this.state.tick }) : null; descriptor.calls += 1; this.state.calls.push({ kind, name, phase, tick: this.state.tick }); return { status: "verified", value: value ?? null, state_hash: this.hash() }; } catch (error) { descriptor.failures += 1; this.state.errors.push({ code: "callback_exception", tick: this.state.tick }); return { status: "blocked", code: "callback_exception", detail: String(error?.message ?? error), state_hash: this.hash() }; } finally { this.state.reentrant = false; } } filter(name, source, args = []) { if (typeof source !== "string" || new TextEncoder().encode(source).length > MAX_SOURCE_BYTES) return this.fail("filter_source_limit"); if (/\b(?:subprocess|socket|os\.system|device_paths|arbitrary_host_paths)\b/.test(source)) return this.fail("real_hardware_excluded"); if (!Array.isArray(args) || !finiteJson(args)) return this.fail("filter_arguments_invalid"); return this.invoke("filter", name, "filter", [source, ...args]); } updateVcp(name, pinName, value) { const descriptor = this.state.vcp[name]; const pin = descriptor?.pins.find((item) => item.name === pinName); if (!pin) return this.fail("vcp_pin_missing"); let normalized; try { normalized = typedValue(pin.type, value); } catch (error) { return this.fail("vcp_value_invalid", String(error.message)); } const previous = pin.value; pin.value = normalized; const result = this.invoke("vcp", name, "pin", [pinName, normalized]); if (result.status !== "verified") pin.value = previous; return result; } restore(value) { if (!value || value.schema !== CALLBACK_SCHEMA) throw new Error("callback_restore_schema"); const unsigned = { ...value }; delete unsigned.state_hash; if (value.state_hash !== fnv1a64(unsigned)) throw new Error("callback_restore_hash"); this.state = clone(unsigned); this.handlers = new Map(); return this.export(); } }