@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
424 lines • 17 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 { AsyncLineIterator } from "../../io/async_line_iterator.js";
import { abortable, makeAbortError, sleep } from "../../utils/abort.js";
import { concat } from "../../utils/bytes.js";
import { boundSystemClock, systemNow } from "../../utils/clock.js";
import { answerOverBridge, failureText, respond, serviceWorkerBridge, } from "./transport.js";
/** A port over a worker, its listener swapped per run. */
function portOf(post, attach, terminate, bridge) {
let receive = () => undefined;
attach((message) => {
receive(message);
});
return {
post,
listen: (fn) => {
receive = fn;
},
terminate,
...(bridge !== undefined ? { bridge } : {}),
};
}
const NO_WORKER = 'a wasm guest runs on a worker, which this page cannot start: it needs shared memory ' +
'(a cross-origin isolated page) or mirage’s service worker bridge';
/** Why no guest worker can start here, or null when one can. */
export async function workerUnavailable() {
const node = globalThis.process?.versions
?.node;
if (node !== undefined || typeof SharedArrayBuffer !== 'undefined')
return null;
if (typeof Worker === 'undefined')
return NO_WORKER;
return (await serviceWorkerBridge()) === null ? NO_WORKER : null;
}
/** A worker for one guest at a time, or null where none can start: a page
* that shares no memory and has no service worker to bridge its calls. */
async function startWorker() {
const node = globalThis.process?.versions
?.node;
if (node !== undefined) {
const { Worker } = await import('node:worker_threads');
const url = new URL(import.meta.url.endsWith('.ts') ? './worker.ts' : './worker.js', import.meta.url);
const worker = new Worker(url, {
execArgv: url.pathname.endsWith('.ts')
? ['--experimental-transform-types', '--disable-warning=ExperimentalWarning']
: [],
});
return portOf((m) => {
worker.postMessage(m);
}, (receive) => {
worker.on('message', receive);
worker.on('error', (error) => {
receive(error);
});
}, () => {
void worker.terminate();
});
}
if (typeof Worker === 'undefined')
return null;
const bridge = typeof SharedArrayBuffer === 'undefined' ? await serviceWorkerBridge() : undefined;
if (bridge === null)
return null;
// The worker starts from the bundle core embeds, as pyodide's does: a
// bundler that moves this module leaves no file a relative URL finds.
const { default: source } = await import("../../generated/wasm_worker.js");
const url = URL.createObjectURL(new Blob([source], { type: 'text/javascript' }));
const worker = new Worker(url, { type: 'module' });
return portOf((m) => {
worker.postMessage(m);
}, (receive) => {
worker.onmessage = (event) => {
receive(event.data);
};
worker.onerror = (event) => {
receive(new Error(event.message || 'its script did not start'));
};
}, () => {
worker.terminate();
URL.revokeObjectURL(url);
}, bridge);
}
/** A row as it crosses to the guest: its original backend row stays here. */
function crossing(row) {
if (row === null)
return null;
return Object.fromEntries(Object.entries(row).filter(([key]) => key !== 'source'));
}
/**
* Compile-once, run-many WASI module, each run on a worker. Mirrors
* Python's `WasmExecution`: the module compiles once, each request runs
* on a worker of its own (an idle one when there is), and a stopped run
* ends its worker, the only way to stop a guest that never calls out.
*
* Filesystem imports are answered here: every fd and path call the guest
* makes lands in its `WasiFs`, whose view and open files are this run's
* `WasmView` and its descriptors, so the run sees exactly what the view
* serves, only the passed environment, and no network.
*
* @param source - the module's bytes.
* @param trapPrefix - the command name a wasm trap's stderr line starts with.
*/
export class WasmExecution {
source;
trapPrefix;
compiled = null;
idle = [];
closed = false;
constructor(source, trapPrefix) {
this.source = source;
this.trapPrefix = trapPrefix;
}
/** The compiled module; a load that failed is tried again on the next run. */
module() {
this.compiled ??= this.source()
.then((bytes) => WebAssembly.compile(bytes))
.catch((error) => {
this.compiled = null;
throw error;
});
return this.compiled;
}
/**
* Run the module once and return `[stdout, stderr, exitCode]`. A guest
* write to a stream with a sink waits until the sink takes it, so a
* reader that falls behind holds the guest back, as a full pipe would;
* that stream then comes back empty. A stop ends the guest where it is:
* the files it left open land, and the run rejects with the stop.
*
* @param argv - full argv, the program name included.
* @param stdin - the run's input, which the guest takes as it reads it.
* @param env - the environment as `[name, value]` pairs.
* @param view - the files the guest sees.
* @param output - receives stdout as the guest writes it; absent returns it.
* @param errors - the same for stderr.
* @param stop - ends the run.
*/
async run(argv, stdin, env, view, output, errors, stop) {
this.checkOpen();
const module = await abortable(this.module(), stop);
const port = this.idle.pop() ?? (await this.started(stop));
const reader = stdin === null ? null : new AsyncLineIterator(stdin);
const kept = [[], []];
const open = new Map();
const buffers = new Set();
const answers = new Set();
// A call still waiting when the run ends (input that has not come)
// ends with it.
const ended = new AbortController();
const halt = stop === undefined ? ended.signal : AbortSignal.any([stop, ended.signal]);
const state = { stopped: false };
const operation = async (call) => {
const args = call.args ?? [];
switch (call.op) {
// The view's files stop with the run, and the run's end waits for
// a call under way, as Python's abort joins it, so a file it
// opens is among those the end lands.
case 'view':
return this.viewCall(view, open, call.method ?? '', args);
case 'file':
return this.fileCall(open, call.file ?? -1, call.method ?? '', args);
case 'stdin':
return reader === null
? new Uint8Array()
: abortable(reader.read1(args[0]), halt);
case 'output': {
const fd = args[0] === 2 ? 2 : 1;
const bytes = args[1];
const sink = fd === 1 ? output : errors;
if (sink === undefined) {
;
(fd === 1 ? kept[0] : kept[1]).push(bytes);
return true;
}
try {
await abortable(sink(bytes), halt);
return true;
}
catch (error) {
if (halt.aborted)
throw error;
// A reader that went away fails the guest's write with EPIPE.
console.debug(`${this.trapPrefix}: the reader went away: ${String(error)}`);
return false;
}
}
case 'sleep':
return sleep(args[0], halt).then(() => false, () => true);
}
};
let fail = (error) => {
throw error;
};
const answer = (message) => {
if (message.buffer !== undefined)
buffers.add(message.buffer);
const work = () => operation(message);
const done = message.buffer !== undefined
? respond(message.buffer, work)
: answerOverBridge(port.bridge ?? '', message.id ?? '', work);
const settled = done
.catch((error) => {
// An answer that cannot reach the guest leaves it waiting forever.
fail(error instanceof Error ? error : new Error(failureText(error)));
})
.finally(() => {
answers.delete(settled);
if (message.buffer !== undefined)
buffers.delete(message.buffer);
});
answers.add(settled);
};
let end = () => undefined;
const result = new Promise((resolve, reject) => {
fail = (error) => {
state.stopped = true;
port.terminate();
reject(error);
};
port.listen((message) => {
if (message instanceof Error)
reject(message);
else if (message.kind === 'vfs')
answer(message);
else if (message.kind === 'result')
resolve(message);
else if (message.kind === 'error')
reject(new Error(message.message));
});
end = () => {
state.stopped = true;
port.terminate();
// A call the guest waits on never sees its answer now.
for (const buffer of buffers) {
const cells = new Int32Array(buffer, 0, 4);
Atomics.store(cells, 0, -1);
Atomics.notify(cells, 0);
}
reject(makeAbortError(stop));
};
});
if (stop?.aborted === true)
end();
else
stop?.addEventListener('abort', end, { once: true });
try {
port.post({
module,
argv,
env: env.map(([name, value]) => `${name}=${value}`),
piped: stdin !== null,
offset: boundSystemClock() === null ? 0 : systemNow() - Date.now() / 1000,
...(port.bridge !== undefined ? { bridge: port.bridge } : {}),
});
const done = await result;
let stderr = concat(kept[1]);
const lines = [...(done.trap === null ? [] : [done.trap]), ...done.lost];
if (lines.length > 0)
stderr = concat([
stderr,
new TextEncoder().encode(lines.map((line) => `${this.trapPrefix}: ${line}\n`).join('')),
]);
const exitCode = done.lost.length > 0 && done.exitCode === 0 ? 1 : done.exitCode;
return [concat(kept[0]), stderr.byteLength > 0 ? stderr : null, exitCode];
}
catch (error) {
// A worker that failed is not reused.
state.stopped = true;
throw error;
}
finally {
stop?.removeEventListener('abort', end);
ended.abort();
await Promise.all(answers);
// A stopped guest left its files open; what they wrote still lands,
// as a killed process's writes are in its files. A landing that fails
// has no one left to tell.
for (const file of open.values()) {
try {
await file.close();
}
catch (error) {
console.debug(`${this.trapPrefix}: landing an open file failed: ${String(error)}`);
}
}
if (state.stopped || this.closed)
port.terminate();
else
this.idle.push(port);
}
}
checkOpen() {
if (this.closed)
throw new Error('WASM execution is closed');
}
/** A started worker, or the stop; one that starts after the stop goes. */
async started(stop) {
const starting = this.ready();
try {
return await abortable(starting, stop);
}
catch (error) {
if (stop?.aborted === true)
void starting.then((port) => {
port.terminate();
}, (failure) => {
console.debug(`${this.trapPrefix}: a stopped run's worker failed: ${String(failure)}`);
});
throw error;
}
}
/** A started worker, listening; one that fails to start is let go,
* its blob URL with it. */
async ready() {
const port = await startWorker();
if (port === null)
throw new Error(NO_WORKER);
try {
await new Promise((resolve, reject) => {
port.listen((message) => {
if (message instanceof Error)
reject(message);
else if (message.kind === 'ready')
resolve();
});
});
}
catch (error) {
port.terminate();
throw error;
}
return port;
}
async viewCall(view, open, method, args) {
const path = args[0];
switch (method) {
case 'stat':
return crossing(await view.stat(path));
case 'lstat':
return crossing(await view.lstat(path));
case 'statOrNull':
return crossing(await view.statOrNull(path, args[1] === true));
case 'open': {
const file = await view.open(path, args[1]);
open.set(file.fd, file);
return { id: file.fd, opened: file.opened, inode: file.inode };
}
case 'unlink':
return view.unlink(path);
case 'mkdir':
return view.mkdir(path);
case 'rmdir':
return view.rmdir(path);
case 'rename':
return view.rename(path, args[1]);
case 'symlink':
return view.symlink(path, args[1]);
case 'walked':
return view.walked(path, args[1] !== false);
case 'readlink':
return view.readlink(path);
case 'setattr':
return view.setattr(path, args[1], args[2], args[3] === true);
case 'readdir':
return view.readdir(path);
}
throw new Error(`unknown view call: ${method}`);
}
async fileCall(open, id, method, args) {
const file = open.get(id);
if (file === undefined)
throw new Error(`no open file ${String(id)}`);
switch (method) {
case 'path':
return file.path;
case 'position':
return file.pos;
case 'size':
return file.size();
case 'read':
return file.read(args[0]);
case 'pread':
return file.pread(args[0], args[1]);
case 'write':
return file.write(args[0]);
case 'pwrite':
return file.pwrite(args[0], args[1]);
case 'seek':
return file.seek(args[0], args[1]);
case 'truncate':
return file.truncate(args[0]);
case 'land':
return file.land();
case 'close':
open.delete(id);
return file.close();
case 'fstat':
return crossing(await file.files.fstat(file.fd));
case 'unlinked':
return file.files.unlinked(file.fd);
}
throw new Error(`unknown file call: ${method}`);
}
/** Let the idle workers go. */
close() {
this.closed = true;
for (const port of this.idle)
port.terminate();
this.idle = [];
return Promise.resolve();
}
}
//# sourceMappingURL=execution.js.map