@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
385 lines • 15.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 { memoryRoom } from "../../../io/memory.js";
import { materialize } from "../../../io/types.js";
import { concat } from "../../../utils/bytes.js";
import { boundSystemClock } from "../../../utils/clock.js";
import { mainFilename } from "../execution.js";
import { DEFAULT_PROG, LIMITS, POOL_WORKERS } from "./constants.js";
import { displayError } from "./errors.js";
import { MontyFs } from "./fs.js";
import { loadMontyModule, } from "./loader.js";
const INTERRUPTED = Symbol('interrupted');
const ENC = new TextEncoder();
/**
* Hard-stop one monty worker. The engine offers no cancel: feedRun
* has no signal and session.close() waits for the in-flight turn
* (probed live), so the only way to stop a busy guest is to kill its
* worker process — which poisons the session (the pool discards and
* replaces the worker) and settles the pending feedRun. The pid is
* only readable while the session is idle, so callers capture it
* before feeding. Python's runtime does the same reclaim: cancelling
* a feed leaves the worker running, so its `_kill_worker` SIGKILLs it
* on cancellation exactly like this.
*/
function killWorker(pid) {
if (pid === undefined)
return;
const proc = globalThis
.process;
if (typeof proc?.kill !== 'function')
return;
try {
proc.kill(pid, 'SIGKILL');
}
catch {
// the worker may already have exited; nothing left to reclaim
}
}
/**
* The one pool of monty workers every workspace in the process runs on, so
* an idle workspace holds no worker of its own. The first runtime that needs
* it opens it and the last to let go closes it; `POOL_WORKERS` run at once
* and a run past them waits. Each run still checks out with its own limits.
* Mirrors Python's `_SharedPool`.
*/
class SharedPool {
users = 0;
opened = null;
/** The pool, opened by the first caller; every acquire is released once. */
async acquire() {
this.users += 1;
const opened = (this.opened ??= SharedPool.open());
try {
return await opened;
}
catch (err) {
if (this.opened === opened)
this.opened = null;
await this.release();
throw err;
}
}
static async open() {
const module = await loadMontyModule();
return module.Monty.create({ maxProcesses: POOL_WORKERS });
}
/** Let go of the pool, closing it once no runtime holds it. */
async release() {
this.users -= 1;
const opened = this.opened;
if (this.users > 0 || opened === null)
return;
this.opened = null;
let pool;
try {
pool = await opened;
}
catch (err) {
console.debug('monty: the pool being opened at close failed', err);
return;
}
await pool.close();
}
}
const sharedPool = new SharedPool();
/**
* Runs on pooled monty workers: the process's shared pool, or one of its own
* for a run nested in another, which must not queue behind its parent's
* worker.
*/
export class MontyExecution {
shared;
name = 'monty';
limits;
module = null;
pool = null;
poolPromise = null;
/**
* Args:
* maxMemory: the heap one run may use, in bytes; null for no cap.
* shared: run on the process's shared pool.
*/
constructor(maxMemory = null, shared = false) {
this.shared = shared;
this.limits = maxMemory === null ? LIMITS : { ...LIMITS, maxMemory };
}
/**
* Run one program on a pooled worker.
*
* @param program the source and argv to run.
* @param ctx the run's context: working directory, environment, signal
* and limit.
* @param files the run's file adapter; null outside a workspace.
* @param output receives stdout as it is printed; absent returns it in
* the result.
* @param errors the same for stderr.
* @param externalLookup the host functions the program may call by name.
*/
async run(program, ctx, files, output, errors, externalLookup = {}) {
// Monty executes on its own worker process, so the event loop stays
// live to observe the limit deadline and the kill signal; a trip
// SIGKILLs the worker (see killWorker). Deadline -> exit 124 via
// CommandTimeoutError, kill -> exit 1 like the local runtime. Both count
// from before the checkout, which waits while every worker of the shared
// pool is busy.
const interruption = this.installInterruption(ctx.signal, ctx.timeoutSeconds);
try {
// A script is Monty's own script_name, which it keeps the last part of
// for `__file__`, under the directory a feed starts in.
const checkout = this.checkout(program.scriptPath === undefined ? undefined : mainFilename(program, ctx.cwd));
const session = await Promise.race([checkout, interruption.promise]);
if (session === INTERRUPTED) {
// A worker that arrives after the run was stopped goes straight back.
void checkout
.then((late) => late.close())
.catch((err) => {
console.debug('monty: a checkout that arrived after its run stopped failed', err);
});
return this.interrupted(interruption, ctx);
}
const workerPid = session.workerPid;
const prints = new Prints(output, errors);
try {
// The prints are handed on inside the race, so a deadline or a kill
// still stops a run whose reader holds its last writes back. The
// kill ends the worker, not a host callback still at work (a scan
// lists directory after directory), so the file calls stop too.
const guestFiles = files?.withSignal(interruption.signal) ?? null;
const run = this.feedOne(session, program, ctx, guestFiles, prints, externalLookup).then(async (outcome) => {
await prints.end();
return outcome;
});
const winner = await Promise.race([run, interruption.promise]);
if (winner !== INTERRUPTED) {
const [exitCode, note] = winner;
const [stdout, stderr] = prints.streams();
return {
stdout,
stderr: note === '' ? stderr : concat([stderr ?? new Uint8Array(), ENC.encode(note)]),
exitCode,
};
}
prints.stop();
run.catch(() => undefined);
killWorker(workerPid);
return this.interrupted(interruption, ctx);
}
finally {
await session.close();
}
}
finally {
interruption.dispose();
}
}
// How a stopped run ends: past its deadline it throws, killed it exits 1.
interrupted(interruption, ctx) {
if (interruption.timedOut.value && ctx.timeoutSeconds !== undefined) {
throw new CommandTimeoutError(this.name, ctx.timeoutSeconds);
}
return { stdout: new Uint8Array(), stderr: null, exitCode: 1 };
}
installInterruption(signal, timeoutSeconds) {
const timedOut = { value: false };
let timer = null;
let removeAbort = null;
const stop = new AbortController();
const promise = new Promise((resolve) => {
const interrupt = () => {
stop.abort();
resolve(INTERRUPTED);
};
if (timeoutSeconds !== undefined && timeoutSeconds > 0) {
timer = setTimeout(() => {
timedOut.value = true;
interrupt();
}, timeoutSeconds * 1000);
}
if (signal !== undefined) {
if (signal.aborted) {
interrupt();
return;
}
const onAbort = () => {
interrupt();
};
signal.addEventListener('abort', onAbort, { once: true });
removeAbort = () => {
signal.removeEventListener('abort', onAbort);
};
}
});
return {
promise,
timedOut,
signal: stop.signal,
dispose: () => {
if (timer !== null)
clearTimeout(timer);
if (removeAbort !== null)
removeAbort();
},
};
}
async close() {
// A pool still being opened must be awaited before teardown, or its
// workers outlive the runtime that asked for them. One that fails to
// open has nothing to close; the command that asked already reported it.
if (this.pool === null && this.poolPromise !== null) {
try {
this.pool = await this.poolPromise;
}
catch (err) {
console.debug('monty: the pool being opened at close failed', err);
}
}
this.poolPromise = null;
if (this.pool !== null) {
const pool = this.pool;
this.pool = null;
if (this.shared)
await sharedPool.release();
else
await pool.close();
}
}
async ensurePool() {
if (this.pool !== null)
return this.pool;
const opening = (this.poolPromise ??= this.loadPool());
try {
this.pool = await opening;
}
catch (err) {
if (this.poolPromise === opening)
this.poolPromise = null;
throw err;
}
return this.pool;
}
async loadPool() {
if (this.shared)
return sharedPool.acquire();
const module = await this.loadModule();
return module.Monty.create();
}
async loadModule() {
this.module ??= await loadMontyModule();
return this.module;
}
async checkout(scriptName) {
const pool = await this.ensurePool();
const room = memoryRoom();
const own = this.limits.maxMemory;
return pool.checkout({
...(scriptName === undefined ? {} : { scriptName }),
limits: room === null
? this.limits
: { ...this.limits, maxMemory: own === undefined ? room : Math.min(own, room) },
assertMessageAnnotations: false,
// A workspace's system_time answers the wall clocks (MontyFs).
osPolicy: boundSystemClock() === null
? { sleepSystemMax: Infinity }
: { sleepSystemMax: Infinity, datetime: 'call_host' },
});
}
/** Feed the program to a worker; its status and what to add to its stderr. */
async feedOne(session, program, ctx, files, prints, externalLookup) {
const module = await this.loadModule();
// Monty has no `sys.stdin`, so piped bytes ride in as a global the
// same way argv does: raw bytes, null when nothing was piped.
const stdin = program.stdin === null ? null : await materialize(program.stdin);
const options = {
// argv[0] is the program's own name when the caller has one (a
// CLI install's head word), else the interpreter's placeholder.
// `??`, not `||`: "" is CPython's own argv[0] for a program piped in
// with no operand, so an empty prog is an answer.
inputs: { argv: [program.prog ?? DEFAULT_PROG, ...program.args], stdin },
cwd: ctx.cwd.virtual,
printCallback: prints.write,
os: new MontyFs(module, ctx.env ?? {}, files).handle,
...(Object.keys(externalLookup).length > 0 ? { externalLookup: { ...externalLookup } } : {}),
};
try {
await session.feedRun(program.code, options);
}
catch (caught) {
if (caught instanceof module.MontySyntaxError || caught instanceof module.MontyRuntimeError) {
const inner = caught.exception;
if (inner?.typeName === 'SystemExit') {
return inner.message ? [1, inner.message + '\n'] : [0, ''];
}
return [1, displayError(caught) + '\n'];
}
if (caught instanceof module.MontyCrashedError) {
return [1, `${this.name}: worker ${caught.timedOut ? 'timed out' : 'crashed'}\n`];
}
throw caught;
}
return [0, ''];
}
}
/**
* What a program prints, in the order it printed it: each write handed to
* its stream's sink as it comes, or kept for the result where the stream
* has none.
*
* Monty calls back without waiting, so a write for a sink is chained
* behind the one before it; the first that fails stops the rest, and
* `end` reports it.
*/
class Prints {
sinks;
kept = { stdout: [], stderr: [] };
writing = Promise.resolve();
failure = null;
stopped = false;
/**
* @param output where stdout goes; absent keeps it.
* @param errors where stderr goes; absent keeps it.
*/
constructor(output, errors) {
this.sinks = { stdout: output, stderr: errors };
}
write = (stream, text) => {
const sink = this.sinks[stream];
if (sink === undefined) {
this.kept[stream].push(text);
return;
}
this.writing = this.writing
.then(() => (this.failure === null && !this.stopped ? sink(ENC.encode(text)) : undefined))
.catch((error) => {
this.failure ??= { error };
});
};
/** Skip the writes still queued once the run is interrupted. */
stop() {
this.stopped = true;
}
/** Wait until every write has been handed on. */
async end() {
await this.writing;
if (this.failure !== null)
throw this.failure.error;
}
/** The stdout and stderr kept for the result, null for no stderr. */
streams() {
const err = this.kept.stderr;
return [ENC.encode(this.kept.stdout.join('')), err.length > 0 ? ENC.encode(err.join('')) : null];
}
}
//# sourceMappingURL=execution.js.map