@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
152 lines • 7.3 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 { CommandTimeoutError } from "../../../errors/types.js";
import { HOME_CONFIG_KEYS, resolveHome, } from "../../config.js";
import { RuntimeFiles } from "../../files.js";
import { mergeSignals } from "../../../utils/abort.js";
import { WasmExecution, workerUnavailable } from "../../wasm/execution.js";
import { loadWasm } from "../../wasm/loader.js";
import { WasmView } from "../../wasm/view.js";
import { JsRuntime } from "../base.js";
import { QuickJsUnavailableError } from "./errors.js";
import { cwdPreamble } from "./execution.js";
export const QUICKJS_HOME_ENV = 'MIRAGE_QUICKJS_HOME';
const WASM_NAME = 'qjs-wasi.wasm';
const BUILD_HINT = `the quickjs runtime needs a ${WASM_NAME} module: download a WASI build of quickjs-ng ` +
'from https://github.com/quickjs-ng/quickjs/releases, and point the runtime entry’s ' +
'config `home` (yaml `runtimes: [{name: quickjs, config: {home: ...}}]`) or the ' +
`${QUICKJS_HOME_ENV} environment variable at the directory (a URL on a page) holding it`;
const ENC = new TextEncoder();
const DEC = new TextDecoder();
/**
* Run JavaScript on a WASI quickjs-ng, the engine the Python host runs.
*
* A bare modern JS engine (ES2023 syntax, ES modules, `JSON`, regex,
* `Promise`, top-level await) inside a wasm sandbox: no node builtins,
* no `require`, no npm, no network. The `std`/`os` globals are exposed
* (quickjs-ng `--std`), so scripts read stdin with
* `std.in.readAsString()` and reach files with `std.open`/`os.readdir`.
* The engine's filesystem imports are intercepted, so file I/O routes
* through the workspace dispatch, with the same cache, write modes and
* session narrowing as shell commands.
*
* Each run is on a worker of its own (`WasmExecution`), so a stopped run
* ends its worker, however busy the guest. The module comes from the
* config `home` or the MIRAGE_QUICKJS_HOME environment variable, a
* directory on Node and a URL on a page. Mirrors Python's `QuickJsRuntime`.
*/
export class QuickJsRuntime extends JsRuntime {
name = 'quickjs';
// The engine is a WASI guest whose `std.open`/`os.readdir` wait on the
// workspace bridge: guest I/O has no way around the gate.
reach = 'workspace';
filesystem = ['read', 'write', 'list', 'stat'];
constructor(options = {}) {
super(options, HOME_CONFIG_KEYS);
}
async openEngine() {
const home = resolveHome(this.config, QUICKJS_HOME_ENV);
if (home === undefined)
throw new QuickJsUnavailableError(BUILD_HINT);
const unavailable = await workerUnavailable();
if (unavailable !== null)
throw new QuickJsUnavailableError(unavailable);
return new WasmExecution(async () => {
try {
return await loadWasm(home, WASM_NAME);
}
catch (err) {
throw new QuickJsUnavailableError(`no ${WASM_NAME} under ${home}; ${BUILD_HINT}`, {
cause: err,
});
}
}, 'js');
}
closeEngine(engine) {
return engine.close();
}
async version(env, signal) {
const engine = await this.engine();
const [stdout, stderr, exitCode] = await engine.run(['qjs', '--version'], null, Object.entries(env), new WasmView(), undefined, undefined, signal);
return {
stdout: exitCode === 0
? ENC.encode(`JavaScript (quickjs-ng ${DEC.decode(stdout).trim()})\n`)
: stdout,
stderr,
exitCode,
};
}
/**
* Run one program on the install's engine.
*
* @param engine - the install's compiled module and its workers.
* @param program - the source and argv to run.
* @param ctx - the run's context.
* @param output - receives stdout as it is written; absent returns it.
* @param errors - the same for stderr.
*/
async interpret(engine, program, ctx, output, errors) {
const module = ctx.flags.module === true;
// --std exposes the std/os globals (stdin via std.in); -m selects
// ES-module mode for .mjs sources, and --script names classic mode for
// the rest, since qjs takes -e source for a module unless told (a
// sloppy `x = 5` then fails strict). Trailing args become scriptArgs.
// A named program takes scriptArgs[0], the slot qjs fills with a
// script's path when it runs a file; unnamed -e leaves the args alone.
// `--` is load-bearing: qjs's -e does not end its option parsing, so a
// program argument that spells a qjs switch would be read as one.
const named = program.prog !== undefined && program.prog !== '' ? [program.prog] : [];
const cwd = ctx.cwd;
let source = program.code;
if (cwd.virtual !== '/') {
// Evaluate scripts separately so their directive prologue still
// controls strict mode. Modules are already strict.
if (module && source.startsWith('#!'))
source = `//${source.slice(2)}`;
if (!module)
source = `std.evalScript(${JSON.stringify(source)});`;
source = cwdPreamble(cwd) + source;
}
const argv = [
'qjs',
'--std',
module ? '-m' : '--script',
'-e',
source,
'--',
...named,
...program.args,
];
const deadline = ctx.timeoutSeconds !== undefined && ctx.timeoutSeconds > 0
? AbortSignal.timeout(Math.min(Math.ceil(ctx.timeoutSeconds * 1000), 2 ** 31 - 1))
: undefined;
const stop = mergeSignals(ctx.signal, deadline);
const files = RuntimeFiles.of(ctx, this.binding);
try {
const [stdout, stderr, exitCode] = await engine.run(argv, program.stdin, Object.entries(ctx.env ?? {}), new WasmView(stop === undefined ? files : (files?.withSignal(stop) ?? null)), output, errors, stop);
return { stdout, stderr, exitCode };
}
catch (err) {
if (ctx.signal?.aborted === true) {
// A cancelled run's stop is no error of the program's: it ends as a
// killed one does, saying nothing, as Monty's does.
return { stdout: new Uint8Array(), stderr: null, exitCode: 1 };
}
if (deadline?.aborted === true && ctx.timeoutSeconds !== undefined)
throw new CommandTimeoutError(this.name, ctx.timeoutSeconds);
throw err;
}
}
}
//# sourceMappingURL=runtime.js.map