@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
169 lines • 7.65 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 { 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