UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

297 lines • 10.9 kB
// ========= 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