@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
232 lines • 10.6 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 { UsageError } from "../../../commands/errors.js";
import { readFailExitCode } from "../../../commands/spec/usage.js";
import { sessionAccess } from "../../../context/session_context.js";
import { formatFsError } from "../../../errors/render.js";
import { CommandTimeoutError } from "../../../errors/types.js";
import { PipeClosed } from "../../../io/errors.js";
import { IOResult } from "../../../io/types.js";
import { WorkspaceRuntime } from "../../../runtime/workspace.js";
import { encodeText } from "../../../shell/bytes.js";
import { CANNOT_EXECUTE_STATUS, NOT_FOUND_STATUS } from "../../../shell/constants.js";
import { PathSpec } from "../../../types.js";
import { makeAbortError, mergeSignals } from "../../../utils/abort.js";
import { Files } from "../../files.js";
import { MountCommandAmbiguous } from "../../mount/errors.js";
import { MountCommandUnsupported } from "../../mount/registry.js";
import { envSnapshot, sessionView } from "../../session/state.js";
import { ExecutionNode } from "../../types.js";
import { parseFlags } from "./flags.js";
/**
* The entry point a command handler runs a nested line through (`opts.shell`):
* the line runs in the calling command's own session, under its signal,
* reading the input it is handed.
*/
export function nestedShell(executeFn, session, signal) {
return (line, stdin = null) => executeFn(line, {
sessionId: session.sessionId,
session,
stdin,
...(signal !== undefined ? { signal } : {}),
});
}
/**
* find's start points: the path operands typed before its expression.
* The expression's start is mirage-rs's, so a word inside it (an `-exec`
* command word, a `-newer` reference) is never a start point even when
* the rest slot's PATH kind would have read it as one. Only the head is
* parsed against the spec, so what it yields as path operands is exactly
* the start points.
*/
export function findStartPoints(argv, start, spec, cwd) {
const head = argv.slice(0, start);
return parseFlags(head, spec, 'find', cwd).paths;
}
/**
* Build the recorded execution node, materializing any streamed stderr.
*
* The node is a recorded artifact, so the live lazy stderr is materialized
* to concrete bytes here, and the bytes stay on `io` so the caller still
* reads them. A result whose fields fill in as its output drains (a
* runtime's run) brings the node up to date once they have. Mirrors
* Python's `exec_node`.
*
* @param command original command text for the record.
* @param io the command's result, whose stderr and status the node carries.
* @param paths the classified path operands.
*/
export async function execNode(command, io, paths = []) {
const node = new ExecutionNode({
command,
stderr: await io.materializeStderr(),
exitCode: io.exitCode,
paths,
});
const state = io.output;
if (state !== null && !state.settled) {
state.callbacks.push(() => {
settled(node, io);
});
}
return node;
}
/** Record what a result settled as its output drained. Mirrors Python's `_settled`. */
function settled(node, io) {
node.exitCode = io.exitCode;
if (io.stderr instanceof Uint8Array)
node.stderr = io.stderr;
}
/** The 126 result for a command no runtime accepted. */
export function admissionDenial(cmdName) {
const msg = `${cmdName}: no runtime accepted this line\n`;
return new IOResult({ exitCode: CANNOT_EXECUTE_STATUS, stderr: encodeText(msg) });
}
/**
* Resolve a command against the line's routing decision: its binding,
* or the decision's fallback when no entry captures it. A resolved
* WorkspaceRuntime means the executor serves the command itself (the
* workspace runtime has no interpreter entry point); null means no runtime
* accepted it: exit 126, "no runtime accepted this line", like a shell
* refusing to exec.
*/
function lineRuntimeFor(cmdName, routingDecision) {
const runtime = Object.hasOwn(routingDecision.bindings, cmdName)
? routingDecision.bindings[cmdName]
: routingDecision.fallback;
if (runtime === null || runtime === undefined)
return [undefined, admissionDenial(cmdName)];
if (runtime instanceof WorkspaceRuntime)
return [undefined, null];
return [runtime, null];
}
// `multiple: true` on find value-flags makes parseToKwargs emit arrays;
// bespoke backend wrappers read these as scalars. Migrated backends read the
// expression from `texts` and ignore flagKwargs.
function scalarFindFlags(flagKwargs) {
const out = { ...flagKwargs };
for (const [key, value] of Object.entries(out)) {
if (Array.isArray(value)) {
const last = value.at(-1);
if (last !== undefined)
out[key] = last;
}
}
return out;
}
// Run one already-parsed command on the mount that owns its paths. The shared
// single-mount execution tail: mount resolution, session-mode checks, runCommand,
// filesystem-error formatting, ls/find post-processing, and read/write key
// prefixing. handleCommand uses it for the normal path, and passes it (bound)
// to the cross-mount runners so each operand executes natively on its owning
// mount. `resolveHint` names the path whose mount runs the command, ahead of
// the first of `paths`: a stream command in stdin mode has none, and awk over
// operands on several mounts runs where its first file lives. A pre-resolved
// `mount` skips resolution and session-mode checks, which the caller already
// performed.
export async function runOnMount(ctx, cmdName, paths, texts, flagKwargs, opts = {}) {
const { registry, context, dispatch, namespace } = ctx;
const session = context.session;
const hint = opts.resolveHint ?? null;
let mount = opts.mount ?? null;
if (mount === null) {
const resolvePaths = hint !== null ? [hint] : paths;
try {
mount = await registry.resolveMount(cmdName, resolvePaths, session.cwd, opts.argv, sessionAccess(session));
}
catch (err) {
if (err instanceof MountCommandUnsupported || err instanceof MountCommandAmbiguous) {
const errBytes = encodeText(`${err.message}\n`);
return [null, new IOResult({ exitCode: 1, stderr: errBytes })];
}
throw err;
}
if (mount === null) {
const errBytes = encodeText(`${cmdName}: command not found`);
return [null, new IOResult({ exitCode: NOT_FOUND_STATUS, stderr: errBytes })];
}
}
let flags = flagKwargs;
if (cmdName === 'find')
flags = scalarFindFlags(flags);
const routing = context.frame.routing;
if (routing === undefined)
throw new Error(`${cmdName}: run outside a routed line`);
const [lineRuntime, denial] = lineRuntimeFor(cmdName, routing);
if (denial !== null)
return [null, denial];
const signal = mergeSignals(mergeSignals(ctx.signal, ctx.context.frame.abortSignal), opts.signal);
// A leaf that resumes here after the caller aborted must not reach a
// mount handler: eager write handlers do not read the signal, and a
// cancelled `rm` must not run.
if (signal?.aborted === true)
throw makeAbortError(signal);
const access = sessionAccess(session);
try {
return await mount.runCommand(cmdName, paths, texts, flags, {
admission: ctx.admission,
...(registry.diff === undefined ? {} : { diff: registry.diff }),
ids: registry.ids,
stdin: opts.stdin ?? null,
cwd: PathSpec.fromStrPath(session.cwd),
vfs: new Files(dispatch, namespace === undefined ? null : namespace.readers(() => session.visibility), null, { follow: ctx.follow ?? null, ledger: false }),
env: envSnapshot(session),
session: sessionView(session, registry.policies, context.frame.diagnostics),
...(registry.processView === undefined ? {} : { processes: registry.processView(session) }),
runtime: {
execAllowed: registry.isExecAllowed(access),
execPathAllowed: (virtual) => registry.execAllowedAt(virtual, access),
...(lineRuntime !== undefined
? { bound: lineRuntime, settle: () => registry.settle(lineRuntime) }
: {}),
},
...(signal !== undefined ? { signal } : {}),
...(ctx.executeFn !== undefined
? { shell: nestedShell(ctx.executeFn, session, signal) }
: {}),
...(opts.argv !== undefined ? { argv: opts.argv } : {}),
...(opts.find !== undefined ? { find: opts.find } : {}),
});
}
catch (err) {
// Command-owned usage errors (extra operands, missing patterns) become
// this command's IOResult so the rest of the line keeps running, like a
// real shell (#452).
if (err instanceof UsageError) {
return [
null,
new IOResult({
exitCode: err.exitCode,
stderr: encodeText(`${err.message}\n`),
}),
];
}
// A limit timeout is not a filesystem failure: let it reach the
// workspace-level handler that answers with exit 124. A write into a
// pipe whose reader has gone is SIGPIPE, which the executor answers for
// the writer.
if (err instanceof CommandTimeoutError ||
err instanceof PipeClosed ||
(err instanceof Error && err.name === 'AbortError'))
throw err;
return [
null,
new IOResult({
exitCode: readFailExitCode(cmdName, err),
stderr: formatFsError(cmdName, err, paths),
}),
];
}
}
//# sourceMappingURL=run.js.map