@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
297 lines • 10.9 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 { SHELL_SPECS, parseShellOptions } from "../../commands/spec/shell.js";
import { getText, literalWord } from "../../shell/helpers.js";
import { NodeType as NT } from "../../shell/types.js";
import { hasGlob, markEscapedGlobs } from "../../utils/glob_walk.js";
import { patternShape } from "../../utils/fnmatch.js";
import { parseBashArgs } from "../executor/builtins/script/bash.js";
import { CondParser, PATTERN_OPS, condLeaves, condTokens } from "./test_tokens.js";
function wordGlobs(node) {
if (node.type === NT.WORD || node.type === NT.NUMBER) {
return hasGlob(patternShape(markEscapedGlobs(getText(node))));
}
if (node.type === NT.COMMAND_NAME || node.type === NT.CONCATENATION) {
return node.namedChildren.some(wordGlobs);
}
return node.type === NT.EXTGLOB_PATTERN;
}
/** Read a word without losing which glob characters are quoted. */
export function readWord(node, home = null) {
return { raw: getText(node), text: literalWord(node, home), glob: wordGlobs(node) };
}
/**
* A `[[ ]]` as the tests the gate admits, read with the tokens and the
* grammar the run evaluates it by: a test per leaf, each admitted as the run
* reaches it, since a branch never reached never expands; every reader of
* the line reads the same leaves, so a question one asked is the one the
* gate answers. (`[` is a command, admitted on its words as any command
* is.) `home` is the home directory a leading tilde names. Mirrors Python's
* reads_of_test.
*/
export function readsOfTest(node, home = null) {
const opener = { raw: '[[', text: '[[' };
return condLeaves(new CondParser(condTokens(node)).parse()).map((leaf) => {
let words;
if (leaf.kind === 'unary') {
words = [{ raw: leaf.op, text: leaf.op }, readWord(leaf.operand, home)];
}
else if (leaf.kind === 'binary') {
words = [
readWord(leaf.left, home),
{ raw: leaf.op, text: leaf.op },
readWord(leaf.right, home),
];
}
else {
words = [readWord(leaf.operand, home)];
}
const stated = leaf.kind !== 'binary' || !PATTERN_OPS.has(leaf.op);
return { words: [opener, ...words], stated };
});
}
/** The text the gate works with: the literal when it has one, the word as typed otherwise. */
export function wordValue(word) {
return word.text ?? word.raw;
}
function valueAt(args, index) {
const word = args[index];
return word === undefined ? '' : wordValue(word);
}
const UNREADABLE = { line: null, argv: [], open: false };
function asLine(line, open = false) {
return { line, argv: [], open };
}
function asArgv(argv, open = false) {
return argv.length > 0 ? [{ line: null, argv, open }] : [];
}
/** Whether the gate can read what runs. */
export function innerReadable(inner) {
return inner.line !== null || inner.argv.length > 0;
}
const FIND_EXEC = new Set(['-exec', '-execdir', '-ok', '-okdir']);
const ENV_FLAGS = new Set(['-i', '--ignore-environment', '-0', '--null', '-']);
function tail(args, count) {
return count > 0 ? args.slice(args.length - count) : [];
}
// `command [-pVv] name [arg ...]`: `-v`/`-V` only report.
function commandInner(args) {
let i = 0;
while (i < args.length && valueAt(args, i).startsWith('-')) {
const word = valueAt(args, i);
i += 1;
if (word === '--')
break;
if (word.includes('v') || word.includes('V'))
return [];
}
return asArgv(args.slice(i));
}
// `exec [-cl] [-a name] [command [arg ...]]`.
function execInner(args) {
let i = 0;
while (i < args.length && valueAt(args, i).startsWith('-')) {
const word = valueAt(args, i);
i += 1;
if (word === '--')
break;
if (word.endsWith('a'))
i += 1;
}
return asArgv(args.slice(i));
}
// `env [-i] [-u NAME]... [NAME=VALUE]... [command [arg]...]`, read the
// way the tree's `env` builtin reads it.
function envInner(args) {
let i = 0;
while (i < args.length) {
const word = valueAt(args, i);
if (word === '--') {
i += 1;
break;
}
if (ENV_FLAGS.has(word) || word.startsWith('--unset=')) {
i += 1;
continue;
}
if (word === '-u' || word === '--unset') {
i += 2;
continue;
}
if (word.startsWith('-') && word.length > 1) {
// A cluster ending in `u` takes the next word as the name.
i += word.endsWith('u') ? 2 : 1;
continue;
}
break;
}
while (i < args.length) {
const word = valueAt(args, i);
if (!word.includes('=') || word.startsWith('='))
break;
i += 1;
}
return asArgv(args.slice(i));
}
// The operands of a shell builtin with a spec, as Words; null when the
// line fails its own option parse.
function specOperands(spec, args) {
const parsed = parseShellOptions(SHELL_SPECS[spec], args.map(wordValue));
if (parsed.invalid !== null || parsed.needsValue !== null || parsed.unexpectedValue !== null) {
return null;
}
return tail(args, parsed.operands.length);
}
// `timeout [OPTION] DURATION COMMAND [ARG]...`.
function timeoutInner(args) {
const operands = specOperands('timeout', args);
if (operands === null || operands.length < 2)
return [];
return asArgv(operands.slice(1));
}
// `xargs [OPTION]... [COMMAND [INITIAL-ARGS]]`, `echo` when none, items
// from stdin appended.
function xargsInner(args) {
const operands = specOperands('xargs', args);
if (operands === null)
return [];
const argv = operands.length > 0 ? operands : [{ raw: 'echo', text: 'echo' }];
return asArgv(argv, true);
}
// `mapfile -C callback`: the callback is evaluated per quantum.
function mapfileInner(args) {
const parsed = parseShellOptions(SHELL_SPECS.mapfile, args.map(wordValue));
const callback = parsed.flags.C;
return typeof callback === 'string' ? [asLine(callback, true)] : [];
}
// `find ... -exec COMMAND [ARG]... ;` (and `-execdir`, `-ok`, `-okdir`,
// `+`), the matched paths appended.
function findInner(args) {
const inner = [];
let i = 0;
while (i < args.length) {
if (!FIND_EXEC.has(valueAt(args, i))) {
i += 1;
continue;
}
i += 1;
const start = i;
while (i < args.length && ![';', '+'].includes(valueAt(args, i)))
i += 1;
for (const item of asArgv(args.slice(start, i), true))
inner.push(item);
}
return inner;
}
// `nice [-n N] COMMAND [ARG]...`.
function niceInner(args) {
let i = 0;
while (i < args.length && valueAt(args, i).startsWith('-')) {
const word = valueAt(args, i);
i += 1;
if (word === '--')
break;
if (word === '-n' || word === '--adjustment')
i += 1;
}
return asArgv(args.slice(i));
}
// `time [-p] COMMAND [ARG]...`.
function timeInner(args) {
let i = 0;
while (i < args.length && ['-p', '--'].includes(valueAt(args, i)))
i += 1;
return asArgv(args.slice(i));
}
// `nohup` and `builtin` run the remaining words verbatim after an
// optional leading `--`.
function argvInner(args) {
const first = args[0];
return asArgv(first !== undefined && wordValue(first) === '--' ? args.slice(1) : args);
}
// `sh`/`bash`: `-c` names the program and `--help` or `--version` runs
// nothing; a script file, a program read from stdin and a line mirage's
// parse refuses are lines the gate cannot read, since a real bash takes
// options mirage does not (`--restricted`, `-O extglob`) and still runs the
// program.
function shellInner(args) {
// Expansion can erase or split a word, shifting an option's value
// onto --version or -c. Only a literal prefix can establish what
// runs; dynamic positional words after its program cannot change it.
const literal = [];
for (const word of args) {
if (word.text === null || word.glob)
break;
literal.push(word.text);
}
const parsed = parseBashArgs(literal);
if (parsed.help || parsed.version)
return [];
if (parsed.script !== null)
return [asLine(parsed.script)];
return [UNREADABLE];
}
/**
* The lines a command runs on the words it was given, for the words
* that run other words.
*
* The table is the workspace shell's own re-dispatchers (every builtin
* that hands a constructed line back to the evaluator: `eval`, `source`,
* `command`, `env`, `timeout`, `xargs`, `mapfile -C`, `sh`/`bash`, an
* executed path) plus the classic prefix runners a real shell has and
* the workspace shell does not (`builtin`, `exec`, `nohup`, `nice`,
* `time`, `find -exec`). A whole-line runtime is a real shell, so
* anything else that
* can run a command (an interpreter's `-c`, `make`, `git` hooks) is the
* runtime's own world: the allow list is the closed form there.
*/
export function innerLines(head, args) {
if (head.includes('/'))
return [UNREADABLE];
switch (head) {
case 'eval':
return args.length > 0 ? [asLine(args.map(wordValue).join(' '))] : [];
case 'source':
case '.':
return args.length > 0 ? [UNREADABLE] : [];
case 'sh':
case 'bash':
return shellInner(args);
case 'command':
return commandInner(args);
case 'builtin':
case 'nohup':
return argvInner(args);
case 'exec':
return execInner(args);
case 'env':
return envInner(args);
case 'timeout':
return timeoutInner(args);
case 'xargs':
return xargsInner(args);
case 'mapfile':
case 'readarray':
return mapfileInner(args);
case 'find':
return findInner(args);
case 'nice':
return niceInner(args);
case 'time':
return timeInner(args);
default:
return [];
}
}
//# sourceMappingURL=inner_lines.js.map