UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

179 lines • 7.35 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 { 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