@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
342 lines • 13.8 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 { CD_ANCHORS, DECL_PRINTER_HEADS, IMPLICIT_HEAD_READS, NAMEREF_HEADS } from "./constants.js";
import { commandArgs, commandInvocations, literalText, sameNode, walkNamedOutsideDefs, } from "./names.js";
function declarationParts(node) {
const words = node.children.filter((c) => !c.type.endsWith('_redirect'));
const first = words[0];
const head = first !== undefined ? first.text : '';
const flags = [];
const operands = [];
for (const child of words.slice(1)) {
if (child.type === 'word') {
if (child.text.startsWith('-'))
flags.push(child.text);
else
operands.push(child);
}
else if (child.isNamed === true || node.namedChildren.some((n) => sameNode(n, child))) {
operands.push(child);
}
}
return { head, flags, operands };
}
function flagHas(flags, letter) {
return flags.some((flag) => flag.startsWith('-') && !flag.startsWith('--') && flag.slice(1).includes(letter));
}
/**
* Names an `env` invocation provably keeps from the environment it
* hands on: null when it reads no existing name at all, else the set a
* whole-environment read may skip.
*
* Scanned with the builtin's own option grammar: `--` ends the
* options, `-u`/`--unset` consume a value (so `-u -i` unsets a
* variable named `-i` rather than clearing) and add it to the
* exclusions, the leading `NAME=VALUE` operands override and exclude
* their names, and the first other operand ends the scan. `-i`,
* `--ignore-environment` or the lone `-` empties the start entirely,
* and an option the builtin refuses stops it from running at all; both
* answer null. The scan is left to right like the builtin's, so
* everything consumed before the first word no static read can spell
* keeps its effect whatever that word turns out to be, and nothing
* after it is claimed: a dynamic word may be the command, demoting
* every later word to an argument.
*/
function envExclusions(args) {
const excluded = new Set();
let i = 0;
while (i < args.length) {
const arg = args[i];
const literal = arg === undefined ? null : literalText(arg);
if (literal === null)
return excluded;
if (literal === '--') {
i += 1;
break;
}
if (literal === '-i' || literal === '--ignore-environment' || literal === '-')
return null;
if (literal === '--unset') {
if (i + 1 >= args.length)
return null;
const next = args[i + 1];
const value = next === undefined ? null : literalText(next);
if (value !== null)
excluded.add(value);
i += 2;
continue;
}
if (literal.startsWith('--unset=')) {
excluded.add(literal.slice('--unset='.length));
i += 1;
continue;
}
if (literal === '-0' || literal === '--null') {
i += 1;
continue;
}
if (literal.startsWith('--'))
return null;
if (literal.startsWith('-') && literal.length > 1) {
let step = 1;
let bad = false;
for (let pos = 1; pos < literal.length; pos += 1) {
const ch = literal[pos];
if (ch === 'i')
return null;
if (ch === 'u') {
const rest = literal.slice(pos + 1);
if (rest !== '') {
excluded.add(rest);
}
else if (i + 1 < args.length) {
const next = args[i + 1];
const value = next === undefined ? null : literalText(next);
if (value !== null)
excluded.add(value);
step = 2;
}
else {
return null;
}
break;
}
if (ch !== '0') {
bad = true;
break;
}
}
if (bad)
return null;
i += step;
continue;
}
break;
}
while (i < args.length) {
const arg = args[i];
const literal = arg === undefined ? null : literalText(arg);
if (literal === null)
return excluded;
if (!literal.includes('=') || literal.startsWith('='))
break;
excluded.add(literal.split('=', 1)[0] ?? '');
i += 1;
}
return excluded;
}
/**
* Names a command's assignment prefixes provably override.
*
* `TOKEN=local printenv TOKEN` hands the command an environment whose
* TOKEN is the override, so an environment read through that
* invocation cannot observe the standing value whatever the override
* expands to; the value's own reads are the walk's business. `+=`
* appends to the standing value and proves nothing.
*/
function prefixAssignmentNames(node) {
const out = new Set();
for (const child of node.namedChildren) {
if (child.type !== 'variable_assignment')
continue;
if (child.children.some((part) => part.type === '+='))
continue;
const nameNode = child.childForFieldName?.('name') ?? null;
if (nameNode?.type !== 'variable_name')
continue;
if (nameNode.text !== '')
out.add(nameNode.text);
}
return out;
}
/**
* How the line's environment-rendering commands read names.
*
* Returns `{whole, names, excluded}`: whether some command renders the
* whole environment, the names printing forms read explicitly, and the
* names every whole-environment read provably skips. Only a printing
* form selects everything: `env` on any invocation (bare it prints
* every exported name, and with arguments it hands the snapshot to the
* command it runs) unless a literal `-i`, `--ignore-environment` or
* lone `-` proves it starts empty, a bare `set`, a bare `printenv`,
* and a declaring builtin with no operands (`export`, `declare -p`).
* `printenv NAME` and `declare -p NAME` read exactly the named
* variables, and a mutating form (`export NAME=v`, `declare -x NAME`,
* `set -u`) reads nothing here, so an unavailable source cannot fail
* the write that would replace its pointer. A print target no static
* read can spell (`printenv $x`) falls back to the whole environment.
*
* Exclusions are per invocation: an assignment prefix overrides its
* name for exactly that command's environment, and `env`'s `-u`,
* `--unset` and `NAME=VALUE` words remove or override theirs
* (`envExclusions`), so `env -u TOKEN printenv TOKEN` cannot observe
* TOKEN however the whole snapshot is handed on. A print target so
* excluded is dropped rather than reported. `excluded` is the
* intersection across the node's whole-environment reads, because a
* name is skippable only when every such read skips it.
*/
export function envReads(node) {
let whole = false;
let excluded = null;
const names = new Set();
for (const current of walkNamedOutsideDefs(node)) {
if (current.type === 'command') {
const nameNode = current.childForFieldName?.('name') ?? null;
const head = nameNode !== null ? nameNode.text : '';
const prefix = prefixAssignmentNames(current);
let skipped = null;
if (head === 'env') {
const scanned = envExclusions(commandArgs(current));
if (scanned !== null)
skipped = new Set([...prefix, ...scanned]);
}
else if (head === 'set') {
if (commandArgs(current).length === 0)
skipped = prefix;
}
else if (head === 'printenv') {
let readAny = false;
for (const child of commandArgs(current)) {
const literal = literalText(child);
if (literal === null) {
skipped = prefix;
readAny = true;
}
else if (!literal.startsWith('-')) {
if (!prefix.has(literal))
names.add(literal);
readAny = true;
}
}
if (!readAny)
skipped = prefix;
}
if (skipped !== null) {
whole = true;
const skip = skipped;
const prior = excluded;
// The parameter annotation breaks tsc's circular inference:
// `excluded` is assigned from an arrow whose context is itself.
excluded =
prior === null ? skip : new Set([...prior].filter((name) => skip.has(name)));
}
}
else if (current.type === 'declaration_command') {
const { head, flags, operands } = declarationParts(current);
if (!DECL_PRINTER_HEADS.has(head))
continue;
let selected = false;
if (operands.length === 0) {
selected = true;
}
else if (flagHas(flags, 'p')) {
for (const operand of operands) {
if (operand.type === 'variable_name') {
if (operand.text !== '')
names.add(operand.text);
}
else if (operand.type !== 'variable_assignment') {
selected = true;
}
}
}
if (selected) {
whole = true;
excluded = new Set();
}
}
}
return { whole, names, excluded: excluded ?? new Set() };
}
/**
* Whether the line reads names no static walk can spell.
*
* Two constructs defeat `referencedNames`: an indirect expansion
* (`${!name}` reads the variable the *value* of `name` names, and the
* `${!p*}`/`${!p@}` forms enumerate by prefix), and a nameref declared
* on the line itself (`declare -n r=T; echo $r` reads T before any
* session record says so). A nameref from an earlier line is not
* opaque: the session records its target, which `deref` resolves.
*/
export function opaqueReads(node) {
for (const current of walkNamedOutsideDefs(node)) {
if (current.type === 'expansion' && current.children.some((c) => c.type === '!')) {
return true;
}
if (current.type === 'declaration_command') {
const { head, flags } = declarationParts(current);
if (NAMEREF_HEADS.has(head) && flagHas(flags, 'n'))
return true;
}
}
return false;
}
/**
* The names one `cd` invocation reads implicitly.
*
* Bare `cd` goes to `$HOME`, `cd -` to `$OLDPWD`, and a searchable
* relative operand tries `$CDPATH` first. Option words (`-L`/`-P`/
* `--`) are not operands, and a word no static read can spell may
* expand to any of the shapes, so it selects all three.
*/
function cdReads(args) {
const operands = [];
for (const arg of args) {
if (arg === null)
return new Set(['HOME', 'OLDPWD', 'CDPATH']);
if (arg === '-' || !arg.startsWith('-'))
operands.push(arg);
}
const target = operands[0];
if (target === undefined)
return new Set(['HOME']);
if (target === '-')
return new Set(['OLDPWD']);
if (CD_ANCHORS.some((anchor) => target.startsWith(anchor)) || target === '.' || target === '..') {
return new Set();
}
return new Set(['CDPATH']);
}
/**
* Names the program reads without a `$NAME` in the text.
*
* Tilde expansion resolves `~` and `~/...` against `$HOME` wherever a
* word expands (patterns, `=~`'s regex and redirect targets included),
* and the word scan mirrors the expansion exactly: `~user`, a mid-word
* tilde and a quoted one stay literal. `cd` reads `$HOME` bare, `$OLDPWD` for `-`
* and `$CDPATH` for a searchable relative operand; `read` splits on
* `$IFS`; `getopts` resumes from `$OPTIND` and consults `$OPTERR`.
* These join the fill plan exactly as a spelled reference does, so a
* managed `HOME` fetches for `echo ~` the way it does for `echo $HOME`.
*/
export function implicitReads(node) {
const out = new Set();
for (const current of walkNamedOutsideDefs(node)) {
if ((current.type === 'word' || current.type === 'regex') &&
(current.text === '~' || current.text.startsWith('~/'))) {
out.add('HOME');
}
}
for (const [head, args] of commandInvocations(node)) {
const reads = head === null ? undefined : IMPLICIT_HEAD_READS.get(head);
if (reads !== undefined)
for (const name of reads)
out.add(name);
if (head === 'cd')
for (const name of cdReads(args))
out.add(name);
}
return out;
}
//# sourceMappingURL=env.js.map