@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
179 lines • 7.35 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 { PolicyDenied } from "../../policy/index.js";
import { arrayCount, arrayExtent, arrayGet, arrayHas, arrayWith, } from "../../shell/array.js";
import { ArithError, ReadonlyVariableError } from "../../shell/errors.js";
import { conversionScalar, subscriptIndex, ensureVarVisible, envGet, seedVar, stripKeyQuotes, visibleArrays, visibleAssocs, deref, RandomReader, sessionArith, } from "./state.js";
const ELEMENT_REF = /^([A-Za-z_]\w*)(?:\[([\s\S]+)\])?$/;
/**
* Whether a `name` / `name[sub]` reference names a set value.
*
* What `test -v` asks. A bare name over an array checks element 0 (the
* literal key `"0"` for an associative one), which is GNU's rule;
* `name[@]` and `name[*]` ask whether any element is set. An
* associative subscript is the key verbatim; an indexed one evaluates
* as arithmetic, and its assignments land through `view`
* (`[[ -v a[x=2] ]]` leaves x at 2, as bash does); null lands them
* ungated, outside a workspace.
*/
export async function elementIsSet(session, ref, view = null) {
const match = ELEMENT_REF.exec(ref);
if (match?.[1] === undefined)
return false;
const name = deref(session, match[1]) || match[1];
const sub = match[2];
const amap = visibleAssocs(session)[name];
const arr = visibleArrays(session)[name];
if (sub === undefined) {
if (amap !== undefined)
return amap['0'] !== undefined;
if (arr !== undefined)
return arrayHas(arr, 0);
return envGet(session, name) !== null;
}
if (sub === '@' || sub === '*') {
if (amap !== undefined)
return Object.keys(amap).length > 0;
if (arr !== undefined)
return arrayCount(arr) > 0;
return envGet(session, name) !== null;
}
// The subscript arrives as the operand spelled it, so `test -v
// 'm["x"]'` asks after key `x`, as the resolver reads it in
// arithmetic and as bash removes the quotes.
if (amap !== undefined)
return amap[stripKeyQuotes(sub)] !== undefined;
const scalar = envGet(session, name);
let held;
if (arr !== undefined)
held = arr;
else if (scalar !== null)
held = [scalar];
else
return false;
let idx = await subscriptIndex(session, sub, view);
if (idx < 0)
idx += arrayExtent(held);
return arrayHas(held, idx);
}
/**
* Assign one element (or a bare name resolved as element 0).
*
* The element mechanics are computed on a copy and the landing write
* goes through the session view as the whole variable the write produces, so a
* refused write leaves nothing half-applied and a `preSession` rule
* sees `m[k]=v` as a write to `m`. The subscript arrives already
* expanded: an associative name takes it as the key verbatim, an
* indexed one evaluates it as arithmetic. A null subscript is a bare
* target, which bash resolves as element 0 of an array and a plain
* scalar otherwise. Answers `"ok"`, `"denied"`, `"readonly"`, or
* `"subscript"`; a preSession refusal from the session view propagates so the
* rule's own message reaches the caller.
*/
export async function assignElement(session, view, name, subscript, value, append = false) {
// An element write through a name reference lands on the target.
name = deref(session, name) || name;
try {
ensureVarVisible(session, name);
}
catch (error) {
if (error instanceof PolicyDenied)
return 'denied';
throw error;
}
if (session.readonlyVars.has(name))
return 'readonly';
const amap = session.assocs[name];
let stored;
let assigned = null;
if (amap !== undefined) {
const key = subscript ?? '0';
if (key === '')
return 'subscript';
const updated = { ...amap };
updated[key] = append ? (amap[key] ?? '') + value : value;
stored = updated;
assigned = new Set([key]);
}
else {
let arr = session.arrays[name];
if (subscript === null && arr === undefined) {
stored = append ? (session.env[name] ?? '') + value : value;
}
else {
if (arr === undefined) {
const scalar = conversionScalar(session, name);
// An existing scalar becomes element 0, even when empty: bash
// resolves `x[-1]` against the length-1 array that produces.
arr = scalar === undefined ? [] : [scalar];
}
let idx = subscript === null ? 0 : await subscriptIndex(session, subscript, view);
if (idx < 0)
idx += arrayExtent(arr);
if (idx < 0)
return 'subscript';
const base = append ? arrayGet(arr, idx) : '';
stored = arrayWith(arr, idx, base + value);
assigned = new Set([idx]);
}
}
if (view !== null) {
await view.set(name, stored, true, assigned);
return 'ok';
}
seedVar(session, name, stored);
return 'ok';
}
/**
* Land an arithmetic command's assignments in the order the expression made
* them, each through the session view: a hidden name throws PolicyDenied and the
* ones after it never land, as a readonly name stopped the evaluation itself
* (`let 'X=5, R=3'` leaves X at 5). The draws made after a seed that landed
* settle either way. Mirrors Python's land_arith.
*/
export async function landArith(session, view, writes, reader) {
try {
for (const write of writes) {
ensureVarVisible(session, write.name);
await assignElement(session, view, write.name, write.key, write.value);
}
}
finally {
reader.settle();
}
}
/**
* Evaluate `text` as an arithmetic command or expansion does and land what
* it assigned through `land`, which settles its `RANDOM` draws. Reads
* resolve against the visible env, so a hidden name counts as unset, and
* bash bound the assignments made before an error, which land before the
* error is thrown; a PolicyDenied from `land` wins over it. Mirrors
* Python's landed_arith.
*/
export async function landedArith(session, view, text, land = landArith, nounset = false) {
const reader = new RandomReader(session);
let result;
try {
result = sessionArith(session, text, reader, nounset);
}
catch (err) {
if (err instanceof ArithError || err instanceof ReadonlyVariableError) {
await land(session, view, err.writes, reader);
}
throw err;
}
await land(session, view, result.writes, reader);
return result.value;
}
//# sourceMappingURL=elements.js.map