UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

169 lines • 7.65 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 { runAsShell } from "../../../context/session_context.js"; import { share } from "../../../io/async_line_iterator.js"; import { IOResult } from "../../../io/types.js"; import { encodeText } from "../../../shell/bytes.js"; import { CallStack } from "../../../shell/call_stack.js"; import { NESTING_MAX } from "../../../shell/constants.js"; import { DiscardSignal, ExitSignal, ReturnSignal } from "../../../shell/errors.js"; import { parseFunction } from "../../../shell/helpers.js"; import { wordText } from "../../../types.js"; import { restoreLocals } from "../../session/state.js"; import { ExecutionNode } from "../../types.js"; import { bodyAliases } from "../builtins/alias/alias.js"; import { isCountWord } from "../builtins/shared.js"; import { executeBody, returning } from "../control.js"; import { liftFunctionTraps, restoreFunctionTraps } from "../traps.js"; /** * How many functions may run nested: `FUNCNEST` when it is a number from 1 * to `NESTING_MAX`, `NESTING_MAX` otherwise, where bash would set no * limit. */ function funcnestLimit(session) { const value = session.env.FUNCNEST; if (value === undefined || !isCountWord(value)) return NESTING_MAX; const limit = Number(value.trim()); return limit > 0 && limit < NESTING_MAX ? limit : NESTING_MAX; } export async function runShellFunction(executeNode, cmdName, source, parser, restParts, context, stdin, callStack, jobTable = null, handed = null, decisions = null, // Where each statement writes as it finishes, undefined to return the // body's output. sink, // Runs a trap action as a line of the shell; null where nothing can. executeFn = null) { const session = context.session; const cs = callStack ?? new CallStack(session.shellNesting); // bash refuses the call before it enters the function, and abandons the // rest of the line as it does after an expansion error. const limit = funcnestLimit(session); if (cs.functionDepth >= limit) { throw new DiscardSignal(encodeText(`bash: ${cmdName}: maximum function nesting level exceeded (${String(limit)})\n`)); } cs.admit(cmdName); // The body's statements read the caller's stdin in turn. const scope = parser.fork(); let body; try { const aliases = bodyAliases(session, cmdName); body = parseFunction(source, (line) => scope.parse(line, aliases)); } catch (error) { scope.release(); throw error; } const bodyStdin = share(stdin); // Positional args carry the word as typed ($1 stays sub/a.txt). const textArgs = restParts.map(wordText); cs.push(textArgs, cmdName); let lifted = liftFunctionTraps(session); const outerNames = session.functionNames; if (outerNames !== null) session.functionNames = cs.functionNames(); // One stack: a local shadows the whole record, so the caller's value // and attributes are saved and put back together. const savedLocals = new Map(); // The caller's frame is kept and put back: a function that calls // another and then declares a `local` is still inside a function, and // its own shadows must keep being recorded on its own frame. const outerLocals = session.localVars; session.localVars = savedLocals; session.localFrames.push(savedLocals); // The body is parsed again from its source, so its rows restart at 0; // it expands the aliases its definition saw, or reads them as a parse of // its own when it came from a stored session. It was read apart from the // alias that may have called it, whose guards do not reach it (`alias a=f` // still lets `f`'s body run its own `a`). const outerParse = [session.parseCurrent, session.parseRow]; const outerView = session.aliasView; const outerExpansion = session.aliasExpansion; session.aliasExpansion = null; let site = session.functionSites.get(cmdName); if (site !== undefined && site.source !== source) site = undefined; if (site === undefined) { session.parseSeq += 1; [session.parseCurrent, session.parseRow] = [session.parseSeq, 0]; } else { ; [session.parseCurrent, session.parseRow] = site.mark; } session.aliasView = site?.aliases ?? null; // Its commands stand under the definition's place, on a hand-off of // their own as every re-parse does, so two definitions of one text each // need a nod and a second call runs on the first's. const origin = site?.origin ?? null; const nested = handed !== null && origin !== null ? { claimed: [], parent: handed, origin } : null; const bodyHanded = nested ?? handed; const run = sink === undefined && nested === null ? executeNode : (n, s, i, c, opts) => executeNode(n, s, i, c, { ...(sink === undefined ? {} : { sink }), ...(nested === null ? {} : { handed: nested }), ...opts, }); try { // The body is shell code: the builtins it runs are the shell's, // whatever `xargs` or `env` marked the line that called it. return await runAsShell(async () => { let stdout; let io; let lastExec; try { ; [stdout, io, lastExec] = await executeBody(run, body, context, bodyStdin, cs, jobTable, bodyHanded, decisions, executeFn, sink ?? null); } catch (err) { if (!(err instanceof ReturnSignal)) throw err; stdout = err.stdout; io = new IOResult({ stderr: err.stderr.byteLength > 0 ? err.stderr : null, exitCode: err.exitCode, }); lastExec = new ExecutionNode({ command: cmdName }); } ; [stdout, io] = await returning(executeFn, session, bodyStdin, cs, stdout, io, sink ?? null); lastExec.exitCode = io.exitCode; return [stdout, io, lastExec]; }); } catch (err) { // An `exec` replaced the shell: the actions went with it, so the ones // the body took from its caller do not come back. if (err instanceof ExitSignal && err.replaced !== null) lifted = [null, null]; throw err; } finally { ; [session.parseCurrent, session.parseRow] = outerParse; session.aliasView = outerView; session.aliasExpansion = outerExpansion; if (nested !== null && decisions !== null) decisions.handUp(session.sessionId, nested); scope.release(); cs.pop(); if (session.functionNames !== null) session.functionNames = outerNames; restoreFunctionTraps(session, lifted); restoreLocals(session, savedLocals); session.localFrames.pop(); session.localVars = outerLocals; } } //# sourceMappingURL=functions.js.map