@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
123 lines • 4.67 kB
JavaScript
// ========= 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