@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
520 lines • 25.2 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 { CommandSpec } from "../../../commands/spec/types.js";
import { ARGPARSE_EXIT, directoryRefusal, clapMissingOperands, leafRefusal, } from "../../../commands/cli/refusal.js";
import { missCondition, pathStat } from "../../mount/namespace/probe.js";
import { FileType, wordText, PathSpec } from "../../../types.js";
import { flagOccurrences } from "../../../commands/spec/flag_view.js";
import { CLAP_EXIT, CLI_CONFIG_ENV, GIT_LONG_OPTIONS } from "../../../commands/cli/constants.js";
import { listedNode, nodeHelp, ownsArgv, walk } from "../../../commands/cli/walk.js";
import {} from "../../../runtime/types.js";
import { flagKwargName } from "../../../commands/spec/constants.js";
import { UsageStyle, Argument } from "../../../commands/spec/types.js";
import { IOResult, materialize } from "../../../io/types.js";
import { closeQuietly } from "../../../io/stream.js";
import { envSnapshot } from "../../session/state.js";
import { ExecutionNode } from "../../types.js";
import { resolveLimit } from "../../../policy/index.js";
import { runtimeForLanguage } from "../../../runtime/routing/decide.js";
import { admissionDenial, execNode } from "./run.js";
import { answered, cliVoice, runHandler } from "./handler.js";
import { runOutput, runtimeUnavailable } from "../../../commands/builtin/general/interpreter.js";
import { LanguageRuntime } from "../../../runtime/language.js";
import { WorkspaceRuntime } from "../../../runtime/workspace.js";
import { optionError, parseFlags } from "./flags.js";
import { concat } from "../../../utils/bytes.js";
import { encodeText } from "../../../shell/bytes.js";
import { NOT_FOUND_STATUS } from "../../../shell/constants.js";
// A textual rest operand is a CLI node's pass-through form: parsed under
// unknownIsOperand, it takes the undeclared dashed tokens the node does not
// refuse, which is what a program parsing its own argv needs. 'str', not
// 'path', so nothing is cwd-resolved or routed. Only that parse reads the
// rest kind this way: a GNU command's textual rest is a list of operands,
// which is why basename has one and still refuses an option it does not know.
const PASSTHROUGH_REST = new Argument('args', { metavar: '', nargs: '*' });
/**
* The spec a leaf's argv parses against, and who answers `--help`.
*
* Usually mirage: a leaf declares its grammar, the parser enforces it,
* and `--help` is injected the way argparse's add_help does. Two nodes
* answer for themselves instead. A leaf that declares `--help` asked for
* the flag, so it is delivered rather than intercepted. And a script root
* that declares no grammar (ownsArgv) has the whole line forwarded:
* refusing `--width` on behalf of a program that accepts it would make
* the tier unusable, since a YAML `clis:` entry cannot declare options at
* all. Returns the spec to parse with and whether the injected `--help`
* is mirage's to answer. CLI init accepts instance fields, so each
* spread is a plain init bag (the withHelpSupport pattern).
*/
function parseSpecFor(leaf, style = UsageStyle.ARGPARSE, passthrough = false) {
if (passthrough) {
const spec = leaf;
return [new CommandSpec({ ...spec, arguments: [...leaf.arguments, PASSTHROUGH_REST] }), false];
}
if (!leaf.addHelp || leaf.arguments.some((argument) => argument.names.includes('--help')))
return [leaf, false];
return [listedNode(leaf, style), true];
}
/**
* Pick the workspace entry that runs a script leaf.
*
* A `runtime:` pin names the entry, and the entry must speak the
* script's language, so `runtime: monty` on a `.mjs` fails loud
* instead of feeding JS to a python interpreter. Without a pin the
* program runs where this line runs its language's own interpreter
* (the tier's head word, `python3` or `node`), so the runtime capturing
* that command, or a route policy, places it, and a line every capturer
* refused is refused here too (126). The first entry speaking the
* language serves when the workspace serves that interpreter itself, or
* outside a workspace line, where `routing` is absent
* (runtimeForLanguage); a placement on a runtime that does not run the
* script's language is refused like such a pin. Every other refusal names
* the world so the fix (add or rename an entry) is visible (127). Mirrors
* Python's `_select_runtime`.
*/
function selectRuntime(prog, leaf, entries, routing) {
const script = leaf.script;
if (script === null) {
throw new Error(`selecting a runtime for '${prog}' without a script`);
}
const known = entries.map((entry) => `'${entry.name}'`).join(', ') || 'none';
let chosen;
if (leaf.runtime !== null) {
chosen = entries.find((entry) => entry.name === leaf.runtime) ?? null;
if (chosen === null) {
return [
null,
missing(`${prog}: unknown runtime: '${leaf.runtime}' (workspace runtimes: ${known})`),
];
}
}
else {
const entry = runtimeForLanguage(entries, script.language);
if (entry === null) {
return [
null,
missing(`${prog}: no workspace runtime runs ${script.language} scripts (workspace runtimes: ${known})`),
];
}
const head = entry.constructor.commands?.[0];
chosen =
routing === undefined || head === undefined
? entry
: head in routing.bindings
? routing.bindings[head]
: routing.fallback;
if (chosen === null || chosen === undefined)
return [null, admissionDenial(prog)];
if (chosen instanceof WorkspaceRuntime)
chosen = entry;
}
if (!(chosen instanceof LanguageRuntime) || chosen.language !== script.language) {
return [
null,
missing(`${prog}: runtime '${chosen.name}' does not run ${script.language} scripts`),
];
}
return [chosen, null];
}
/** The 127 a script CLI answers when no entry can run its program. */
function missing(message) {
return new IOResult({ exitCode: NOT_FOUND_STATUS, stderr: encodeText(`${message}\n`) });
}
/**
* Render the invocation onto the selected runtime as one `-c` run.
*
* The script tier's whole contract, the one a native binary could also
* honor: the program is named (argv slot 0, so its own messages read
* `pager:` and a renamed install names itself), re-parses `argv` (the
* verbatim tokens after the head), reads piped stdin, and finds the
* install's config as `MIRAGE_CLI_CONFIG` (JSON) in its environment.
* The output converts through the interpreter handlers' one mapping
* (runOutput), handed over as the program runs.
*
* @param config the install's config, handed to the program as JSON.
* @param ctx the line's context.
* @param script the install's embedded program.
* @param runtime the selected interpreter entry.
* @param prog the installed head word.
* @param settle settles the runtime once the program ends, however it ends.
*/
function scriptOutput(config, ctx, script, runtime, prog, settle) {
const env = { ...ctx.env };
if (config !== null && config !== undefined) {
env[CLI_CONFIG_ENV] = JSON.stringify(config);
}
// A .mjs source needs the engine's module mode, the same bit the js
// command derives from the operand's extension.
const flags = { c: script.source, scriptCli: true };
if (script.module)
flags.module = true;
// Renamed to the installed head word, which the program names itself by.
const spec = ctx.spec ?? {};
const runCtx = {
...ctx,
flags,
env,
spec: new CommandSpec({ ...spec, name: prog }),
};
const run = async (output, errors, stop) => {
try {
return await runtime.execute([], [...(ctx.argv ?? [])], { ...runCtx, signal: stop }, { output, errors });
}
catch (err) {
// The interpreter is missing, not the program: 127, as the
// interpreter command answers for the same runtime.
if (!runtimeUnavailable(err))
throw err;
return {
stdout: new Uint8Array(),
stderr: encodeText(`${prog}: ${err.message}\n`),
exitCode: NOT_FOUND_STATUS,
};
}
finally {
await settle?.(runtime);
}
};
return runOutput(run, null, undefined, ctx.stdin, ctx.signal);
}
/**
* Whether this CLI reaches a service past the dispatcher.
*
* Its write verbs then go past the dispatcher's per-path invalidation, so the
* mounts its client may reach drop their caches after one
* (`MountRegistry.dropCaches`). Two roots do that: one with a `configModel`
* (an account CLI, initialized from it) and a script root, whose config is
* opaque by construction and whose program may reach anything. A root with
* neither (`git`) has no service to reach; its writes go through the
* dispatcher, which invalidates as it goes, so a blanket drop would only cost
* every other mount a reload. Mirrors Python's `reaches_service`.
*/
export function reachesService(spec) {
return spec.configModel !== null || spec.script !== null;
}
/**
* Execute a line whose head word is an installed CLI.
*
* Dispatch is by NAME: the install resolves the program tree and the
* validated config; no mount is consulted and no operand path picks a
* backend (the one executor divergence from mount commands). The walk
* consumes subcommand words and group options; the leaf's own argv
* uses the shared CommandSpec parser. The leaf gets the line's one Context,
* built here and nowhere else: an fn leaf runs as
* `fn(accessor, paths, texts, ctx)`, a script
* leaf runs its embedded program on a workspace runtime
* (scriptOutput), so usage refusals, limits, and classification all
* happen in front of either tier. Help too, for every node that declared
* a grammar to render it from (parseSpecFor). The workspace facts in
* `context` reach a verb on its `ctx`, so a verb that never reads `ctx.vfs`
* cannot touch a mount.
*/
export async function handleCli(install, parts, session, stdin = null, context = {}, dropCaches = null) {
// Taken before the first await, so a removal racing this line waits for
// the leaf and its output rather than closing the accessor under them.
const release = install.activity.acquire();
try {
const [stdout, io, node] = await runCli(install, parts, session, stdin, context, dropCaches);
return [stdout === null ? null : install.activity.hold(stdout), io, node];
}
finally {
release();
}
}
/**
* Walk a line down a command tree to the leaf it names and parse the
* leaf's words, or answer the line without running anything.
*
* The walk consumes subcommand words and group options; the leaf's own argv
* uses the shared CommandSpec parser. Help, usage refusals and an operand
* base that is no directory are answered here, for an installed CLI and a
* tree a mount serves alike. Mirrors Python's `walk_leaf`.
*/
export async function walkLeaf(name, cli, parts, session, vfs) {
// Words re-enter string space as typed (wordText): the walk owns
// interpretation, so a quoted "Lunch?" must not arrive as the
// glob-classified absolute /Lunch?. Leaf path operands are resolved
// later by parseFlags against the session cwd.
const words = parts.map((p) => wordText(p));
const cmdStr = words.join(' ');
const argv = words.slice(1);
// The walk takes the same environment the leaf parse below does, so
// a group-level option declaring `Argument.env` fills at its own
// level; without it the fetched credential never enters groupFlags.
const result = walk(name, cli.spec, argv, session.cwd, envSnapshot(session));
if (result.leaf === null) {
const stderr = result.stream === 'stderr' ? result.output : new Uint8Array(0);
const stdout = result.stream === 'stdout' ? result.output : null;
const io = new IOResult({ exitCode: result.exitCode, stderr });
return [stdout, io, new ExecutionNode({ command: cmdStr, exitCode: result.exitCode, stderr })];
}
if (vfs !== undefined) {
const dispatch = vfs.dispatch;
for (const base of result.operandBases) {
const info = await pathStat(dispatch, base);
if (info !== null && info.type === FileType.DIRECTORY)
continue;
const reason = info === null ? await missCondition(dispatch, base) : 'ENOTDIR';
const [stderr, code] = directoryRefusal(name, base.rawPath, reason, cli.spec.usageStyle);
return [
null,
new IOResult({ exitCode: code, stderr }),
new ExecutionNode({ command: cmdStr, exitCode: code, stderr }),
];
}
}
const prog = [name, ...result.path].join(' ');
const leaf = result.leaf;
const handler = cli.handlers[result.path.join(' ')];
if (handler === undefined)
throw new Error(`cli '${prog}': missing handler`);
// No injected --version: that is a GNU coreutils convention, not an
// argparse one.
const [parseSpec, mirageHelp] = parseSpecFor(leaf, cli.spec.usageStyle, ownsArgv(cli));
// The dialect is the root's, not the leaf's: a program answers in one voice
// at every level.
const style = cli.spec.usageStyle;
// The environment goes into the parse, not on top of it: an option
// declaring one is coerced, choice-checked, path-resolved and credited
// against required exactly as a typed value is.
// git resolves an abbreviated long option against the verb's own full table
// (parse-options), and its revision walkers take whole words only.
const abbreviations = cli.spec.name === 'git' ? (GIT_LONG_OPTIONS.get(result.path.join(' ')) ?? []) : undefined;
const parsed = parseFlags([...result.argv], parseSpec, prog, session.cwd, envSnapshot(session), true, abbreviations);
const { paths, texts, flagKwargs, warnings } = parsed;
if (mirageHelp && flagKwargs.help === true) {
const helpText = encodeText(nodeHelp(prog, parseSpec, style));
return [helpText, new IOResult(), new ExecutionNode({ command: cmdStr, exitCode: 0 })];
}
const refusal = optionError(prog, parsed);
let msg = null;
let shown = null;
let code = 0;
if (refusal !== null) {
;
[msg, code, shown] = leafRefusal(style, refusal[0], parsed, result.path.join(' '), leaf);
}
else if (parsed.missingRequiredOperands.length > 0 && style === UsageStyle.CLAP) {
msg = clapMissingOperands(prog, parseSpec, parsed.missingRequiredOperands, parsed.typedDests, session.env);
code = CLAP_EXIT;
}
else if (parsed.missingRequiredOperands.length > 0 && style === UsageStyle.ARGPARSE) {
const usage = nodeHelp(prog, parseSpec, style).split('\n')[0] ?? '';
const missing = [...new Set(parsed.missingRequiredOperands)].join(', ');
msg = encodeText(`${usage}\n${prog}: error: the following arguments are required: ${missing}\n`);
code = ARGPARSE_EXIT;
}
if (msg !== null) {
return [
shown,
new IOResult({ exitCode: code, stderr: msg.length > 0 ? msg : null }),
new ExecutionNode({ command: cmdStr, exitCode: code, stderr: msg }),
];
}
// Group flags merge into the one bag: ancestor/descendant collisions
// are a build-time CLI error, so a group flag can never shadow a
// leaf flag.
const flags = {};
for (const [spelling, value] of Object.entries(result.groupFlags)) {
flags[flagKwargName(spelling)] = value;
}
Object.assign(flags, flagKwargs);
flagOccurrences(flags).push(...flagOccurrences(flagKwargs));
// Only the injected flag is dropped; a leaf that declared --help
// itself is handed the value it asked for.
if (mirageHelp)
delete flags.help;
return {
cmdStr,
argv,
prog,
path: result.path,
leaf,
handler,
flags,
paths,
texts,
warnings,
};
}
/**
* Execute a line whose head word names a command tree a mount serves.
*
* The tree a VFS was handed (or one registered on its mount) is walked
* exactly as an installed CLI's is (`walkLeaf`: verbs, help, usage
* refusals, the leaf's parse); the leaf then runs on the mount, on its
* accessor and under its refusals (`MountEntry.runLeaf`), with its own
* limit: the profile's, then the mount's and the workspace's entry for the
* verb path, then the leaf's declared one. A tree runs on the mount serving
* its name, never by operand path, and its leaf keys that mount's accessor by
* each operand: the operands the leaf parse resolved are followed through the
* link table as the router follows a mount command's (`follow`), and one
* another mount holds (`owner`) is refused before the leaf runs. Mirrors
* Python's `handle_mount_cli`.
*/
export async function handleMountCli(mount, cli, parts, session, stdin = null, context = {}, owner, follow, prepared) {
const walked = prepared ?? (await walkLeaf(cli.spec.name, cli, parts, session, context.vfs));
if (Array.isArray(walked))
return walked;
const limit = resolveLimit(walked.prog, [], walked.handler.limit, mount.commandLimits.get(walked.prog) ?? null, context.commandLimits, session.commandLimits);
const paths = follow === undefined
? walked.paths
: (await follow([...walked.paths])).filter((p) => p instanceof PathSpec);
const foreign = paths.find((p) => owner !== undefined && p.rawPath !== '-' && owner(p.virtual) !== mount);
if (foreign !== undefined) {
const msg = encodeText(`${walked.prog}: ${foreign.rawPath}: not on ${mount.prefix}, the mount serving ${cli.spec.name}\n`);
return [
null,
new IOResult({ exitCode: 1, stderr: msg }),
new ExecutionNode({ command: walked.cmdStr, exitCode: 1, stderr: msg }),
];
}
const run = (ctx) => mount.runLeaf(walked.prog, cli, walked.handler, paths, walked.texts, ctx);
return runWalked(walked, session, stdin, context, limit, run);
}
/** handleCli's body, run while the install is held. */
async function runCli(install, parts, session, stdin, context, dropCaches) {
const walked = await walkLeaf(install.name, install.cli, parts, session, context.vfs);
if (Array.isArray(walked))
return walked;
const { prog, handler: binding } = walked;
const limit = resolveLimit(prog, [], binding.limit, null, context.commandLimits, session.commandLimits);
const script = install.cli.script;
let start;
if (script !== null) {
const [runtime, refused] = selectRuntime(prog, install.cli, context.entries ?? [], context.routing);
if (runtime === null) {
// The interpreter is missing, not the command: 127 like an
// interpreter command no runtime entry captures, or 126 when this
// line's capturers all refused it.
const stderr = await materialize(refused.stderr);
return [
null,
new IOResult({ exitCode: refused.exitCode, stderr }),
new ExecutionNode({ command: walked.cmdStr, exitCode: refused.exitCode, stderr }),
];
}
start = (ctx) => Promise.resolve(scriptOutput(install.config, ctx, script, runtime, prog, context.settle));
}
else {
const fn = binding.fn;
if (fn === null) {
// validateCli guarantees fn XOR subcommands XOR script and walk
// only returns handler-bearing nodes as leaf; reaching this is a
// bug.
throw new Error(`walk returned a leaf without a handler for '${prog}'`);
}
// Defer the call into the promise: a synchronously-thrown leaf error
// must land in the runner's catch arms, exactly as when the call sat
// inside its try.
start = (ctx) => answered(Promise.resolve().then(() => fn(install.accessor, walked.paths, walked.texts, ctx)));
}
const run = (ctx) => {
const body = start(ctx);
// Racing a promise does not stop its work: a body, or a renderer of the
// rows it handed over, still running past its abort keeps the
// install's accessor until the answer settles.
const settled = install.activity.acquire();
// The leaf's `write` is the one answer: the gate hands it to policy as
// `CommandContext.write`, and the cache reads it too, so a verb that can
// mutate (`gh api` under any method) costs the mounts a reload rather
// than a stale read, and so does one that failed, timed out or was
// cancelled after its request may have landed.
return runHandler(body, prog, { command: prog, prefixes: [], declared: binding.limit ?? null }, cliVoice(prog), {
settle: settled,
...(binding.write && dropCaches !== null ? { drop: dropCaches } : {}),
...(ctx.signal !== undefined ? { signal: ctx.signal } : {}),
...(ctx.vfs !== undefined ? { vfs: ctx.vfs } : {}),
});
};
return runWalked(walked, session, stdin, context, limit, run);
}
/**
* Run a walked leaf with the one Context a leaf gets, and record it.
*
* The leaf's declared limit caps its output, exactly like mount dispatch.
* A failure the runner framed is recorded as it is; an answer gets the
* parse's warnings ahead of its stderr. Mirrors Python's `run_walked`.
*/
export async function runWalked(walked, session, stdin, context, limit, run) {
let active = true;
const ended = () => {
active = false;
};
const shell = async (line, data = null) => {
if (!active || context.signal?.aborted === true) {
throw new Error('CLI shell is no longer active');
}
if (context.shell === undefined)
throw new Error('CLI shell is unavailable');
return context.shell(line, data);
};
// Most CLIs never read the files: an API client has no filesystem, while
// `git` is nothing but one. Absent outside a workspace, so a verb that
// needs a mount refuses there on its own. A leaf at the root is the program
// itself, so its spec takes the name it is installed under.
const leaf = walked.leaf;
const ctx = {
stdin,
flags: walked.flags,
cwd: PathSpec.fromStrPath(session.cwd),
env: envSnapshot(session),
argv: walked.argv,
spec: walked.path.length > 0 ? walked.leaf : new CommandSpec({ ...leaf, name: walked.prog }),
...(context.signal !== undefined ? { signal: context.signal } : {}),
...(context.vfs !== undefined ? { vfs: context.vfs } : {}),
...(context.session !== undefined ? { session: context.session } : {}),
...(context.processes === undefined ? {} : { processes: context.processes }),
...(context.shell !== undefined ? { shell } : {}),
...(context.ids === undefined ? {} : { ids: context.ids }),
};
let out;
try {
out = await run(ctx);
}
catch (err) {
ended();
throw err;
}
const [answer, io] = out ?? [null, new IOResult()];
// A run that starts as its output is read (a script's) may still call
// back, so the shell stays live until that output settles.
if (io.output !== null && !io.output.settled)
io.output.callbacks.push(ended);
else
ended();
const stdout = answer;
if (io.producer === null) {
const stderr = await io.materializeStderr();
return [
stdout,
io,
new ExecutionNode({ command: walked.cmdStr, exitCode: io.exitCode, stderr }),
];
}
if (context.signal?.aborted === true) {
// The caller was released at its abort, so nothing reads this output:
// close it rather than leave its source open.
await closeQuietly(stdout);
await closeQuietly(io.stderr);
}
const prog = walked.prog;
if (walked.warnings.length > 0) {
const warn = encodeText(walked.warnings.map((w) => `${prog}: ${w}\n`).join(''));
const existing = await materialize(io.stderr);
io.stderr = concat([warn, existing]);
}
return [stdout, io, await execNode(walked.cmdStr, io, walked.paths)];
}
//# sourceMappingURL=cli.js.map