@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
393 lines • 16.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 { compareCodePoints } from "../utils/sort.js";
import { BuiltinGroup, BuiltinTier, NodeType, ShellBuiltin } from "./types.js";
export const PARAMETER_NAME = /^(?:[A-Za-z_][A-Za-z0-9_]*|[0-9]+|[@*
// The blanks bash skips between arithmetic tokens.
export const ARITH_BLANKS = ' \t\n';
// A variable name in an expression: ASCII, as bash's names are.
export const ARITH_NAME = /[A-Za-z_][A-Za-z0-9_]*/y;
// An integer constant as bash reads one off an expression: a digit and the
// name characters, `@` and `#` after it, judged whole once read (`1a` and
// `08` are each one constant, too great for their base).
export const ARITH_LITERAL = /[0-9][0-9A-Za-z_@
// Operators longest-first, so `<<=` never lexes as `<<` then `=`.
export const ARITH_OPERATOR = /<<=|>>=|\*\*|\+\+|--|<<|>>|<=|>=|==|!=|&&|\|\||[-+*/%&^|]=|[-+*/%<>=!~&|^?:(),]/y;
export const ARITH_ASSIGN_OPS = new Set([
'=',
'+=',
'-=',
'*=',
'/=',
'%=',
'<<=',
'>>=',
'&=',
'^=',
'|=',
]);
export const ARITH_UNARY_OPS = new Set(['!', '~', '-', '+']);
// Binary operators by how tightly they bind, loosest first; `**` alone
// groups from the right, and a unary operator binds tighter than all of
// them (`-2**2` is 4).
export const ARITH_PRECEDENCE = new Map([
['||', 1],
['&&', 2],
['|', 3],
['^', 4],
['&', 5],
['==', 6],
['!=', 6],
['<', 7],
['<=', 7],
['>', 7],
['>=', 7],
['<<', 8],
['>>', 8],
['+', 9],
['-', 9],
['*', 10],
['/', 10],
['%', 10],
['**', 11],
]);
// How deep variables holding expressions may nest (`x="1+2"; $((x))`): a
// variable read from an expression this many values deep is past the
// limit. bash's is 1023; Python's stack holds about a dozen frames per
// level, so mirage stops at 27, which names the same expression of a
// reference cycle of 1, 2, 3, 4, 6 or 12 names as bash's limit does (27
// and 1023 agree modulo 12).
export const ARITH_MAX_DEPTH = 27;
// How deep the shell may re-enter itself: function calls, sourced files,
// `eval` lines and nested shells (`bash -c`, a script) together. bash limits
// none of them unless FUNCNEST names a limit on functions; mirage serves every
// session from one process, where `f(){ echo $(f); }; f` or a file sourcing
// itself would take the memory they all share, so a re-entry this deep is
// refused, and FUNCNEST may only lower the limit on functions. Python's stack
// holds a body nesting three compound commands (about 30 frames a level) this
// deep.
export const NESTING_MAX = 32;
// The descriptors the shell models: stdin, stdout and stderr, and no
// table above them. A redirect naming any other number is refused before
// it does anything (`shell/descriptors.ts`), because the old fall-through
// aliased fd 3 onto stdout and `exec 3>&-` closed the session's stdout.
// FD_BOTH is `Redirect.fd` for `&>`; FD_CLOSE is `Redirect.target` for
// `>&-`.
export const FD_STDIN = 0;
export const FD_STDOUT = 1;
export const FD_STDERR = 2;
export const FD_BOTH = -1;
export const FD_CLOSE = -1;
// Builtins that read and write no file of their own, so a write redirect on
// one can wait for their output: nothing they do can see a target emptied
// early. `printf -v` assigns a variable instead, and is opened first like any
// other command (`redirect.ts`).
export const OUTPUT_ONLY_BUILTINS = new Set([
'echo',
'printf',
'true',
'false',
':',
]);
// The dynamic variables the shell answers itself: PIPESTATUS reads the
// session's record of the last pipeline (`SessionState.pipeStatus`), FUNCNAME
// the frames on the call stack (`SessionState.functionNames`) and RANDOM
// steps a generator (`session/rng.ts`). None lives in the variable store.
export const PIPESTATUS = 'PIPESTATUS';
export const FUNCNAME = 'FUNCNAME';
export const RANDOM = 'RANDOM';
// bash 5.2's RANDOM generator: a Park-Miller minimal-standard
// step through Schrage's method, the value folding the state's two halves
// and keeping 15 bits, and a draw that never repeats the value before it.
// A seed is the assigned integer truncated to 32 bits, and a zero state
// steps from ZERO_SEED. Identical in both languages, so `RANDOM=42` is the
// same sequence everywhere, and bash's.
export const RANDOM_A = 16807;
export const RANDOM_Q = 127773;
export const RANDOM_R = 2836;
export const RANDOM_M = 0x7fffffff;
export const RANDOM_ZERO_SEED = 123459876;
export const RANDOM_MODULUS = 2 ** 32;
export const RANDOM_MAX = 32767;
// What `SessionState.randomSeed` holds once `unset RANDOM` has stripped the name
// of its meaning: no generated word is ever empty.
export const RANDOM_UNSET = '';
// What the shell calls itself when no script is running, bash's "bash".
// A nested `bash`/`sh` overrides it through SessionState.scriptName, and
// `SessionState.argv0` is the one place the two are folded together.
export const SHELL_ARGV0 = 'mirage';
// The one directory PATH names, bash's default PATH for a shell that
// starts without one: every program a session can run has a file here
// (the /usr/bin view mount), which is the path which, type and command -v
// report.
export const BIN_PREFIX = '/usr/bin';
// The IFS a shell starts with, what an unset IFS splits on, and the
// characters of any IFS that count as its whitespace.
export const IFS_DEFAULT = ' \t\n';
// What bash says when fork(2) fails with EAGAIN, as at `ulimit -u`: the
// forking shell abandons the rest of its line with status 254 (bash 5.2,
// pinned in debian:stable-slim), and a subshell dying of it reports 254 to
// its parent. A session's `processes.max` is the cap here. bash first
// retries with backoff, printing `fork: retry:`; the refusal here is
// immediate.
export const FORK_FAILED = 'bash: fork: Resource temporarily unavailable\n';
export const FORK_FAILED_STATUS = 254;
// The statuses a command ends with when it did not run its course, as bash
// and coreutils report them: GNU timeout's 124 (also a line past its timeout
// budget), 126 for a command found but refused (not executable, a policy
// deny, a runtime that would not take it, an argument list too long), 127 for
// one not found, and 128 + the signal for an interrupt (SIGINT) or a write to
// a closed pipe (SIGPIPE).
export const TIMEOUT_STATUS = 124;
export const CANNOT_EXECUTE_STATUS = 126;
export const NOT_FOUND_STATUS = 127;
export const INTERRUPTED_STATUS = 130;
export const SIGPIPE_STATUS = 141;
// Node types whose failure never triggers `set -e` by shape alone.
// Lists are NOT exempt: bash exits when the command after the final
// `&&`/`||` fails; short-circuit failures set SessionState.errexitImmune
// instead, so the executor loops skip only those.
export const ERREXIT_EXEMPT_TYPES = new Set([NodeType.NEGATED_COMMAND]);
// Statements whose failure the ERR trap does not answer again: a group,
// `if`, a loop and `case` ran it for their own failing command already, and
// an `&&`/`||` list for its right command. A subshell and a function call
// are answered, since the trap is hidden inside them unless `set -E`
// (pinned against bash 5.2.37).
export const ERR_TRAP_EXEMPT_TYPES = new Set([
...ERREXIT_EXEMPT_TYPES,
NodeType.LIST,
NodeType.COMPOUND_STATEMENT,
NodeType.IF_STATEMENT,
NodeType.WHILE_STATEMENT,
NodeType.FOR_STATEMENT,
NodeType.C_STYLE_FOR_STATEMENT,
NodeType.CASE_STATEMENT,
]);
// Every letter bash's `set` accepts, mapped to the `-o` name it is a
// synonym for. The full table is here rather than only the letters
// mirage acts on, because a letter left out is silently dropped: `set -C`
// read as "no such option, ignore" is exactly the silent-accept the
// fail-loud rule exists to stop, and it made noclobber unreachable by its
// own letter while `set -o noclobber` worked.
export const SET_FLAG_TO_OPTION = Object.freeze({
a: 'allexport',
b: 'notify',
e: 'errexit',
f: 'noglob',
h: 'hashall',
k: 'keyword',
m: 'monitor',
n: 'noexec',
p: 'privileged',
t: 'onecmd',
u: 'nounset',
v: 'verbose',
x: 'xtrace',
B: 'braceexpand',
C: 'noclobber',
E: 'errtrace',
H: 'histexpand',
P: 'physical',
T: 'functrace',
});
// Every name GNU's `set -o` accepts, pinned from `set -o` on
// debian:stable-slim. mirage acts on a few and stores the rest, mirroring
// how a cluster letter naming no option is kept rather than refused. A
// name absent from here is the one thing bash rejects outright, and it
// rejects it with exit 2 — which is what keeps a silently-ignored
// `set -o physical` from looking supported.
export const SET_OPTION_NAMES = new Set([
'allexport',
'braceexpand',
'emacs',
'errexit',
'errtrace',
'functrace',
'hashall',
'histexpand',
'history',
'ignoreeof',
'interactive-comments',
'keyword',
'monitor',
'noclobber',
'noexec',
'noglob',
'nolog',
'notify',
'nounset',
'onecmd',
'physical',
'pipefail',
'posix',
'privileged',
'verbose',
'vi',
'xtrace',
]);
// What each option reads as before anything sets it, pinned from
// `bash -c 'set -o'` on debian:stable-slim (5.2.37). Only three are on,
// and all three are on for a non-interactive shell too, so this is the
// table `set -o` prints rather than an interactive shell's.
export const SET_OPTION_DEFAULTS = new Map([...SET_OPTION_NAMES]
.sort(compareCodePoints)
.map((name) => [name, ['braceexpand', 'hashall', 'interactive-comments'].includes(name)]));
// Every name GNU's `shopt` accepts and what it reads as before anything
// sets it, pinned from `bash -c shopt` on debian:stable-slim (5.2.37),
// in bash's own listing order. Kept apart from SET_OPTION_NAMES because
// bash keeps two vocabularies, `set -o` and `shopt`, with `shopt -o` as
// the one bridge.
export const SHOPT_DEFAULTS = new Map([
['autocd', false],
['assoc_expand_once', false],
['cdable_vars', false],
['cdspell', false],
['checkhash', false],
['checkjobs', false],
['checkwinsize', true],
['cmdhist', true],
['compat31', false],
['compat32', false],
['compat40', false],
['compat41', false],
['compat42', false],
['compat43', false],
['compat44', false],
['complete_fullquote', true],
['direxpand', false],
['dirspell', false],
['dotglob', false],
['execfail', false],
['expand_aliases', false],
['extdebug', false],
['extglob', false],
['extquote', true],
['failglob', false],
['force_fignore', true],
['globasciiranges', true],
['globskipdots', true],
['globstar', false],
['gnu_errfmt', false],
['histappend', false],
['histreedit', false],
['histverify', false],
['hostcomplete', true],
['huponexit', false],
['inherit_errexit', false],
['interactive_comments', true],
['lastpipe', false],
['lithist', false],
['localvar_inherit', false],
['localvar_unset', false],
['login_shell', false],
['mailwarn', false],
['no_empty_cmd_completion', false],
['nocaseglob', false],
['nocasematch', false],
['noexpand_translation', false],
['nullglob', false],
['patsub_replacement', true],
['progcomp', true],
['progcomp_alias', false],
['promptvars', true],
['restricted_shell', false],
['shift_verbose', false],
['sourcepath', true],
['varredir_close', false],
['xpg_echo', false],
]);
export const GROUP_TIER = new Map([
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
]);
// One row per ShellBuiltin. shell/types.test.ts pins that the rows cover
// the enum, that every group is used, and that the tier sets below are
// the rows' partition, so a new member has to be filed here on purpose.
export const BUILTIN_GROUP = new Map([
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
[],
]);
export const GRAMMAR_BUILTINS = new Set([...BUILTIN_GROUP].filter(([, g]) => GROUP_TIER.get(g) === BuiltinTier.GRAMMAR).map(([b]) => b));
//# sourceMappingURL=constants.js.map