UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

219 lines • 9.18 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/errors.js"; import { arrayExtent, arrayUnset } from "../../../../shell/array.js"; import { FUNCNAME } from "../../../../shell/constants.js"; import { ArithError } from "../../../../shell/errors.js"; import { sessionEntry } from "../../../session/session.js"; import { deref, envGet, sessionView, subscriptIndex, visibleArrays, visibleAssocs, } from "../../../session/state.js"; import { TARGET_RE } from "../constants.js"; import { flagHelp } from "../help/help.js"; import { builtinUsage, fail, ok, refusal, requireView } from "../shared.js"; /** * Clear the getopts residue after a whole-variable unset. * * The record itself is the view's to drop, scalar or array alike: it * keeps a hidden name, and inside a function it may put back a value an * enclosing scope saved. */ function unsetVariable(session, name) { if (name === 'OPTIND') session.getoptsOptind = null; if (name === FUNCNAME) session.functionNames = null; } /** * `subscriptIndex` whose failure ends the line, in bash's words: `unset * 'a[1/0]'` aborts with `1/0: division by 0`. */ async function fatalIndex(session, subscript, view) { try { return await subscriptIndex(session, subscript, view); } catch (err) { if (err instanceof ArithError) throw err.signal('', true); throw err; } } /** * Clear one array element, or a scalar addressed as `base[0]`. * * Clearing an element keeps the indices of the elements after it, as bash * does: it leaves a hole, which neither expands in `${arr[@]}` nor counts * toward `${#arr[@]}` but keeps `${arr[i]}` addressing the same values. A * subscript on a scalar names element 0 only: `x[0]` unsets the scalar * and any other subscript reports `notarray`. A subscript on a name that * holds nothing at all is a silent no-op, but on an existing array a * negative subscript still below zero after the extent is added reports * `subscript`. * * The element mechanics are the builtin's own, but every landing write * still mutates `base`'s session state, so it clears the plane's gate * first: for an array base the view's env half is empty, so `view.unset` * is exactly the gate, and for a scalar's element 0 it is the whole * unset itself. Validation errors write nothing and so never ask. */ async function unsetElement(session, view, base, subscript) { const amap = sessionEntry(visibleAssocs(session), base); if (amap !== undefined) { // The subscript is the key, verbatim: `unset "m[1+1]"` removes the // key "1+1", and a key that is not there (GNU pins `unset "m[@]"` // on an associative array as this same no-op) answers quietly // without a write. if (!Object.hasOwn(amap, subscript)) return 'ok'; const next = { ...amap }; // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete next[subscript]; await view.set(base, next); return 'ok'; } const arr = sessionEntry(visibleArrays(session), base); if (arr === undefined) { // Visible reads on purpose: a hidden base answers the unset // branch's silent no-op instead of a denial that would leak the // name's existence. if (envGet(session, base) === null) return 'ok'; if ((await fatalIndex(session, subscript, view)) !== 0) return 'notarray'; await view.unset(base); return 'ok'; } let idx = await fatalIndex(session, subscript, view); if (idx < 0) { idx += arrayExtent(arr); if (idx < 0) return 'subscript'; } const next = [...arr]; arrayUnset(next, idx); await view.set(base, next); return 'ok'; } /** * Unset shell variables, arrays, or functions, with bash's flags. * * `-v` targets a variable only, `-f` a function only, and a bare name a * variable if one exists or else a function. A `name[idx]` operand clears * one element; the readonly guard resolves it to the base name first, * since that is what `readonly` records. `-n` unsets a name reference * itself, where a bare name unsets what the reference points at. */ export async function handleUnset(args, session, state = null) { if (args[0] === '--help') { const answered = flagHelp('unset'); if (answered !== null) return answered; } let mode = 'auto'; let i = 0; while (i < args.length && (args[i] ?? '').startsWith('-') && args[i] !== '-') { const tok = args[i] ?? ''; if (tok === '--') { i += 1; break; } if (/^-[vfn]+$/.test(tok)) { if (tok.includes('f')) mode = 'f'; else if (tok.includes('n')) mode = 'n'; else mode = 'v'; i += 1; continue; } return fail('unset', `bash: unset: ${tok}: invalid option\n${builtinUsage('unset')}`, 2); } for (const name of args.slice(i)) { if (mode === 'n') { // `unset -n` drops the reference itself rather than its target; // on a plain variable bash unsets it. Ungated-by-target unset. A // frozen reference refuses, writable target or not. const view = requireView(state); if (view.isReadonly(name, false)) { return fail('unset', `bash: unset: ${name}: cannot unset: readonly variable\n`); } try { await view.unset(name, false); } catch (err) { if (err instanceof PolicyDenied) return refusal('unset', err); throw err; } continue; } if (mode === 'f') { if (session.readonlyFunctions.has(name)) { return fail('unset', `bash: unset: ${name}: cannot unset: readonly function\n`); } session.removeFunction(name); continue; } const match = TARGET_RE.exec(name); const subscript = match?.[2]; const isElement = subscript !== undefined; // `readonly arr` records the base name, so an `arr[i]` operand has to // be resolved before the guard, as bash does (which also names the // base, not the element, in the error). const base = match?.[1] ?? name; if (session.readonlyVars.has(base)) { return fail('unset', `bash: unset: ${base}: cannot unset: readonly variable\n`); } const existed = isElement || name in session.env || name in session.arrays || name in session.assocs; // Both spellings clear the preSession gate for the base name: the // whole-variable unset through the view's env half, an element // unset inside unsetElement, so `unset 'X[0]'` cannot sidestep a // policy that vetoes `unset X`. let status; try { if (subscript !== undefined) { status = await unsetElement(session, requireView(state), base, subscript); } else { await requireView(state).unset(name); unsetVariable(session, deref(session, name) || name); status = 'ok'; } } catch (err) { if (err instanceof PolicyDenied) return refusal('unset', err); throw err; } if (status !== 'ok') { // bash names the base for "not an array variable" but prints only // the bracketed part for a bad subscript. const detail = status === 'notarray' ? `unset: ${base}: not an array variable` : `unset: ${name.slice(base.length)}: bad array subscript`; return fail('unset', `bash: ${detail}\n`); } if (mode === 'auto' && !existed && name in session.functions) { if (session.readonlyFunctions.has(name)) { return fail('unset', `bash: unset: ${name}: cannot unset: readonly function\n`); } session.removeFunction(name); } } return ok('unset'); } /** The `unset` arm. */ export async function unsetBuiltin(call) { return handleUnset([...call.argv.args], call.context.session, sessionView(call.context.session, call.registry.policies, call.context.frame.diagnostics)); } //# sourceMappingURL=unset.js.map