@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
247 lines • 10.7 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 { currentCaller } from "../../../context/session_context.js";
import { UsageError } from "../../../commands/errors.js";
import { StreamRefusal } from "../../../rs/errors.js";
import { readFailExitCode } from "../../../commands/spec/usage.js";
import { errorVirtualPath, fsStrerror } from "../../../errors/fs.js";
import { formatFsError } from "../../../errors/render.js";
import { CommandTimeoutError, GlobLimitError } from "../../../errors/types.js";
import { chunks } from "../../../io/cooperative.js";
import { PipeClosed } from "../../../io/errors.js";
import { OutputStream, closeQuietly } from "../../../io/stream.js";
import { IOResult } from "../../../io/types.js";
import { encodeText } from "../../../shell/bytes.js";
import { STOP_WAIT_SECONDS } from "../../../shell/job_table/constants.js";
import { ABORT_JOIN_MS, runCancellable, stopSignal } from "../../../utils/abort.js";
import { concat } from "../../../utils/bytes.js";
import { readerOf } from "../../files.js";
import { CANNOT_EXECUTE_STATUS } from "../../../shell/constants.js";
/**
* What a failed leaf says after its name: a file error stamped with its
* path as the path and phrase, the way the shell's own commands say it,
* anything else in its own words. Mirrors Python's `_leaf_message`.
*/
function leafMessage(err) {
const strerror = fsStrerror(err);
if (strerror !== null && typeof err.virtualPath === 'string')
return `${errorVirtualPath(err)}: ${strerror}`;
return err instanceof Error ? err.message : String(err);
}
/** Whether `err` is an abort, which the caller that aborted answers. */
function aborted(err) {
return err instanceof Error && err.name === 'AbortError';
}
/**
* What a command given a glob past its mount's `walkLimit` says: exec(2)'s
* refusal of an argument list too long, which bash reports with exit 126, then
* the limit's own line. Mirrors Python's `argument_list_too_long`.
*/
export function argumentListTooLong(command, reason) {
return encodeText(`${command}: Argument list too long\n${command}: ${reason}\n`);
}
/**
* A mount command's failure, worded like GNU (`prog: message`), a file
* error with the path as typed. Mirrors Python's `_mount_failure`.
*/
function mountFailure(name, paths, err) {
if (err instanceof GlobLimitError)
return [null, argumentListTooLong(name, err.message), CANNOT_EXECUTE_STATUS];
const code = err instanceof UsageError ? err.exitCode : readFailExitCode(name, err);
return [null, formatFsError(name, err, paths), code];
}
/**
* A CLI leaf's failure: a usage error in its own words and exit code,
* anything else after the program's name with exit 1, and what the leaf
* printed before it failed kept. Mirrors Python's `_leaf_failure`.
*/
function leafFailure(prog, err) {
const printed = err instanceof StreamRefusal ? err.output : null;
if (err instanceof UsageError)
return [printed, encodeText(`${err.message}\n`), err.exitCode];
return [printed, encodeText(`${prog}: ${leafMessage(err)}\n`), 1];
}
/** The voice of a command a mount runs; `paths` are the operands as typed. */
export function mountVoice(name, paths) {
return {
fail: (err) => mountFailure(name, paths, err),
passes: (err) => err instanceof CommandTimeoutError || err instanceof PipeClosed || aborted(err),
splits: false,
};
}
/** The voice of a CLI leaf, installed or on a mount. */
export function cliVoice(prog) {
return {
fail: (err) => leafFailure(prog, err),
passes: (err) => err instanceof CommandTimeoutError || aborted(err),
splits: true,
};
}
/**
* The caller has gone, so output the handler hands back later has no
* reader: close it, and log a failure no caller is left to see.
*/
function abandon(body, err, name) {
void body.then(async (late) => {
if (late === null)
return;
await closeQuietly(late[0]);
await closeQuietly(late[1].stderr);
}, (late) => {
if (late !== err && !aborted(late))
console.error(`${name}: failed after its caller left: ${String(late)}`);
});
}
/**
* A leaf's answer, an empty one where it returned null. A mount command's
* null declines, so its mount tries the next handler; a leaf's null is its
* whole answer, an empty one. Mirrors Python's `answered`.
*/
export async function answered(body) {
return (await body) ?? [null, new IOResult()];
}
/**
* Run one handler body: a mount command's, or a CLI leaf's.
*
* The body runs until it answers or the caller's signal aborts (the line's
* deadline among the reasons). Racing a promise does not stop its work, so
* output the body hands back after its caller left is closed.
* A usage error is the handler's own words and exit code; any other failure
* is worded in the caller's voice, so the rest of the line keeps running. A
* handler that may have written past the dispatcher drops caches afterwards,
* whether it answered, failed (in its own words too), timed out or was
* cancelled, again when a body (or the renderer of the rows it handed over)
* still running past its abort settles, since its write may land then, and
* again once an answer that streams has ended or
* been closed, since a stream does its work as it is pulled. An answer
* carries the producer the output limits resolve from and
* has its stream framed so a failure while it streams is worded too; a
* failure the runner framed carries no producer. Null is the handler
* declining, which a mount's cascade reads as "try the next handler".
* Rows a handler hands over in place of stdout (`IOResult.rows`) lose those
* the session cannot see and are rendered here, inside the run, so this
* is the one place a command's output meets the session's hides.
* Mirrors Python's `run_handler`.
*/
export async function runHandler(body, name, producer, voice, opts = {}) {
let out;
// The rendered answer is what a caller that left abandons: a renderer that
// finishes late hands back output nobody reads.
const answer = rendered(body, opts.vfs ?? null, name);
if (opts.settle !== undefined)
void answer.then(opts.settle, opts.settle);
// A `kill` names a signal, and the program the command started gets the
// stop bound to answer it (a handler, a sandbox reap), as Python joins
// the cancelled handler whole; any other abort releases the caller soon.
const runner = currentCaller().process;
const grace = () => (runner?.signal ?? stopSignal(opts.signal)) === null ? ABORT_JOIN_MS : STOP_WAIT_SECONDS * 1000;
try {
out = await runCancellable(answer, opts.signal, grace);
}
catch (err) {
abandon(answer, err, name);
const drop = opts.drop;
if (drop !== undefined) {
await drop();
if (err instanceof CommandTimeoutError || opts.signal?.aborted === true) {
const settle = () => drop();
void answer.then(settle, settle).catch((dropErr) => {
// The command already returned, so a drop that fails here (a
// workspace torn down under it) has no stream to land on; it is
// reported rather than left as an unhandled rejection.
const reason = dropErr instanceof Error ? dropErr.message : String(dropErr);
console.warn(`${name}: cache drop after timeout failed: ${reason}`);
});
}
}
if (err instanceof UsageError) {
return [
null,
new IOResult({ exitCode: err.exitCode, stderr: encodeText(`${err.message}\n`) }),
];
}
if (voice.passes(err))
throw err;
const [printed, stderr, exitCode] = voice.fail(err);
return [printed, new IOResult({ exitCode, stderr })];
}
const drop = opts.drop;
if (drop !== undefined)
await drop();
if (out === null)
return null;
const source = out[0];
out[1].producer = producer;
const [stream, io] = opts.scope !== undefined ? opts.scope(out) : out;
if (stream === null || (stream instanceof Uint8Array && !voice.splits))
return [stream, io];
const pulled = framed(stream, io, voice);
const lazy = !(stream instanceof Uint8Array);
return [
new OutputStream(pulled, async () => {
await closeQuietly(source);
await closeQuietly(stream);
if (drop !== undefined && lazy)
await drop();
}),
io,
];
}
/**
* A handler's answer with the rows it handed over rendered: those the
* session cannot see dropped first. Mirrors Python's `_rendered`.
*/
async function rendered(body, vfs, name) {
const out = await body;
if (out === null)
return out;
const [stdout, io] = out;
const rows = io.rows;
if (rows === null)
return out;
if (stdout !== null) {
await closeQuietly(stdout);
throw new Error(`${name}: hands over both rows and stdout`);
}
io.rows = null;
let items = [...rows.items];
if (vfs !== null) {
const reader = readerOf(vfs);
items = items.filter((row) => reader.visible(row.path));
}
return [await rows.render(items), io];
}
/** Keep a deferred failure on its command, after any emitted bytes. */
async function* framed(source, io, voice) {
try {
if (voice.splits || source instanceof Uint8Array)
yield* chunks(source);
else
yield* source;
}
catch (err) {
if (voice.passes(err))
throw err;
const [printed, stderr, exitCode] = voice.fail(err);
if (printed !== null)
yield* chunks(printed);
io.stderr = concat([await io.materializeStderr(), stderr]);
io.exitCode = exitCode;
io.output?.finish();
}
finally {
await closeQuietly(source);
}
}
//# sourceMappingURL=handler.js.map