UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

123 lines • 4.67 kB
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= import source from "../../../generated/pyodide.js"; import { concat } from "../../../utils/bytes.js"; function isProxy(value) { return (value !== null && (typeof value === 'object' || typeof value === 'function') && 'destroy' in value); } const NO_XATTRS = () => JSON.stringify({ code: 'ENOTSUP' }); /** Streams over a run's whole input that keep what it writes. */ function collected(stdin) { const input = stdin ?? new Uint8Array(); const out = []; const err = []; let at = 0; return { out, err, read: (size) => { const bytes = input.subarray(at, Math.min(input.length, at + size)); at += bytes.length; return bytes; }, write: (fd, bytes) => { ; (fd === 1 ? out : err).push(bytes); return true; }, }; } export class PyodideExecution { pyodide; namespace; stdio = collected(null); constructor(pyodide, xattr = NO_XATTRS, subprocess = () => { throw new Error('subprocess requires a Pyodide worker with shared memory'); }, seal = () => undefined) { this.pyodide = pyodide; pyodide.registerJsModule('_mirage_xattr', { call: xattr }); pyodide.registerJsModule('_mirage_process', { run: subprocess }); pyodide.registerJsModule('_mirage_exit', { seal }); // Numbers cross, not buffers: Pyodide's buffer conversion slices with // signed wasm32 addresses, so above 2 GiB it reads the wrong bytes. pyodide.registerJsModule('_mirage_stdio', { read: (size) => Array.from(this.stdio.read(size)), write: (fd, data) => this.stdio.write(fd === 2 ? 2 : 1, Uint8Array.from(data)) ? 0 : -1, }); this.namespace = pyodide.toPy({ __name__: '_mirage_pyodide' }); try { pyodide.runPython(source, { globals: this.namespace, filename: 'mirage/execution.py' }); } catch (error) { this.namespace.destroy(); throw error; } } /** * Run one program. Its output comes back in the result, or, where the * run's streams are given, goes to them as it is written and the * result leaves it out. * * @param request the program and what of the run's context it reads. * @param arm starts the run's interrupt countdown. * @param disarm stops it. * @param stdio the run's streams; absent reads `request.stdin`. */ run(request, arm, disarm, stdio) { const kept = collected(request.stdin); this.stdio = stdio ?? kept; try { const { stdin, ...rest } = request; const exitCode = this.call('run', { ...rest, piped: request.piped ?? stdin !== null, filename: request.filename ?? undefined, }, arm, disarm); return [concat(kept.out), concat(kept.err), exitCode]; } finally { this.stdio = collected(null); } } seedSysPath(paths) { return this.call('seed_sys_path', paths); } close() { this.namespace.destroy(); } call(name, ...args) { const fn = this.namespace.get(name); const converted = []; // A number or a string comes back as itself, anything else a proxy. let result; try { for (const arg of args) converted.push(this.pyodide.toPy(arg)); // PyProxy calls are synchronous so interpreter interrupts reach guest code. result = fn(...converted); return isProxy(result) ? result.toJs({ create_proxies: false }) : result; } finally { if (isProxy(result)) result.destroy(); for (const arg of converted) if (isProxy(arg)) arg.destroy(); fn.destroy(); } } } //# sourceMappingURL=execution.js.map