@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
338 lines • 13.8 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 { concat } from "../../utils/bytes.js";
import { asyncChain } from "../../io/stream.js";
import { IOResult, materialize } from "../../io/types.js";
import { applyBarrier, BarrierPolicy } from "../../shell/barrier.js";
import { CallStack } from "../../shell/call_stack.js";
import { ExitSignal, ReturnSignal } from "../../shell/errors.js";
import { NodeKind, nodeKind } from "../../shell/node_kind.js";
import { isProgramInvocation } from "../../context/session_context.js";
import { asWritten, recordStatus, restoreStatus, snapshotStatus, } from "./statement.js";
import { ERR_TRAP_EXEMPT_TYPES } from "../../shell/constants.js";
import { NodeType as NT } from "../../shell/types.js";
/**
* Start a child shell: `( )`, a pipeline stage, a job, `$( )`.
*
* The session's `exitTrap` is the `trap ... EXIT` action, '' for an
* ignored EXIT. A child shell keeps its parent's with `exitTrapInherited`
* set: it lists it, as bash's `trap -p` does there, and runs none of it
* until it registers its own. It keeps the ERR action hidden unless
* `set -E` and the RETURN action unless `set -T`, and lists them all the
* same. It is live shell state, which a session store keeps none of.
*/
export function inheritTraps(session) {
session.exitTrapInherited = session.exitTrap !== null;
session.trapStatus = null;
if (session.shellOptions.errtrace !== true)
session.errTrapHidden = true;
if (session.shellOptions.functrace !== true)
session.returnTrapHidden = true;
}
/** Start a new shell (`bash -c`, a script): it has no actions. */
export function clearTraps(session) {
session.exitTrap = null;
session.exitTrapInherited = false;
session.trapStatus = null;
session.errTrap = null;
session.returnTrap = null;
session.errTrapHidden = false;
session.returnTrapHidden = false;
session.errTrapRunning = false;
session.returnTrapRunning = false;
}
/**
* Take the caller's ERR and RETURN actions from a function's body unless
* `set -E` / `set -T`: the body neither runs nor lists them. Returns the
* actions taken, for `restoreFunctionTraps`.
*/
export function liftFunctionTraps(session) {
let errTrap = null;
let returnTrap = null;
if (session.errTrap !== null &&
session.errTrap !== '' &&
!session.errTrapHidden &&
session.shellOptions.errtrace !== true) {
errTrap = session.errTrap;
session.errTrap = null;
}
if (session.returnTrap !== null &&
session.returnTrap !== '' &&
!session.returnTrapHidden &&
session.shellOptions.functrace !== true) {
returnTrap = session.returnTrap;
session.returnTrap = null;
}
return [errTrap, returnTrap];
}
/**
* Give the actions `liftFunctionTraps` took back as the function returns,
* each unless the body set one of its own, as bash does.
*/
export function restoreFunctionTraps(session, lifted) {
const [errTrap, returnTrap] = lifted;
if (errTrap !== null && session.errTrap === null)
session.errTrap = errTrap;
if (returnTrap !== null && session.returnTrap === null)
session.returnTrap = returnTrap;
}
/**
* Whether the ERR action is set and seen here, taken as a statement starts:
* bash answers a failure only when the action was armed before the command
* ran, so a function that sets one is not answered for. A line run as a
* program (`env`, `timeout`, `find -exec`, a `/usr/bin` path) is no shell,
* so its statements arm nothing: the shell's statement running it answers
* its failure once.
*/
export function errTrapArmed(session) {
return (session.errTrap !== null &&
session.errTrap !== '' &&
!session.errTrapHidden &&
!isProgramInvocation(session));
}
/**
* Run the ERR action after a statement finished with `status`.
*
* It runs where `set -e` would act: not in a test, the left of `&&`/`||`
* or after `!`, and not again for a group, `if`, a loop, `case` or
* `&&`/`||` list, whose own failing command ran it already, unless the
* statement failed to open a redirect and never ran (`execNode.unopened`,
* which this consumes, so a group returning the same node does not answer
* the failure again). `$?` is
* `status` while it runs and again after it, whatever the action returns,
* and the action does not run while it is running, nor once `set -e` is
* ending the shell. An `exit` or `return` in it leaves as the statement's
* would. Hidden in a function or a child shell unless `set -E`, as bash's
* is. `armed` is `errTrapArmed` as the statement started. Returns what the
* action wrote, for the caller to land; empty when none runs. Mirrors
* Python.
*/
export async function runErrTrap(executeFn, node, status, session, armed, stdin = null, callStack = null, execNode = null) {
const action = session.errTrap;
const unopened = execNode?.unopened === true;
if (execNode !== null)
execNode.unopened = false;
const statement = node.type === NT.REDIRECTED_STATEMENT && node.children[0] !== undefined
? node.children[0]
: node;
if (!armed ||
status === 0 ||
executeFn === null ||
action === null ||
action === '' ||
session.errTrapHidden ||
session.errTrapRunning ||
session.errexitExiting ||
session.errexitImmune ||
session.errexitIgnored ||
(ERR_TRAP_EXEMPT_TYPES.has(statement.type) &&
!unopened &&
nodeKind(statement) !== NodeKind.ARITH))
return [];
session.errTrapRunning = true;
try {
return await runAction(executeFn, action, session, stdin, callStack);
}
finally {
session.errTrapRunning = false;
}
}
/**
* Run the RETURN action as a function or a sourced file returns. It runs in
* the frames of what is returning, with `$?` as the last command left it;
* what returns keeps its own status. Hidden in a function or a child shell
* unless `set -T` or it set its own, and it does not run while it is
* running, nor once `set -e` is ending the shell. Returns what the action
* wrote, for the caller to land; empty when none runs. Mirrors Python.
*/
export async function runReturnTrap(executeFn, session, stdin = null, callStack = null) {
const action = session.returnTrap;
if (executeFn === null ||
action === null ||
action === '' ||
session.returnTrapHidden ||
session.returnTrapRunning ||
session.errexitExiting)
return [];
session.returnTrapRunning = true;
try {
return await runAction(executeFn, action, session, stdin, callStack);
}
finally {
session.returnTrapRunning = false;
}
}
/**
* Run a trap action as a line of the shell and collect its output. `$?` and
* `${PIPESTATUS[@]}` are as they were again after it, whatever the action
* returns, and what the action runs in a test or after `!` leaves the
* `set -e` answer for the statement it answers as it was. A failure `set -e`
* acts on in it ends the shell with its status, and an `exit` or `return` in
* it leaves with what it wrote, for the redirects of its statement to route.
*/
async function runAction(executeFn, action, session, stdin, callStack) {
const held = snapshotStatus(session);
const immune = session.errexitImmune;
let io;
try {
io = await executeFn(action, {
sessionId: session.sessionId,
session,
stdin,
callStack: callStack ?? new CallStack(session.shellNesting),
});
}
catch (err) {
if (err instanceof ExitSignal || err instanceof ReturnSignal)
err.unrouted = true;
throw err;
}
finally {
session.errexitImmune = immune;
}
const stdout = await materialize(io.stdout);
const stderr = await io.materializeStderr();
if (session.errexitExiting) {
const ended = new ExitSignal(io.exitCode, stderr, stdout);
ended.unrouted = true;
throw ended;
}
restoreStatus(session, held, session.statusWriter);
return asWritten(stdout, stderr);
}
/**
* Run the shell's EXIT action as the shell ends with `status`.
*
* bash clears the action before it runs it, and an `exit` in it, or one
* it registers there, does not run again. `$?` starts at `status`, and a
* bare `exit` keeps it. The status the shell ends with stays `status`
* unless the action exits, or fails where `set -e` acts (not in a test,
* the left of `&&`/`||` or after `!`), which exits too; its commands
* answer ERR and RETURN afresh. Hard stops (cancellation, a killed job, a
* closed workspace) are not an end the shell reaches, and never get here.
* `callStack` holds the frames the action runs in: the function that
* called `exit` is still on them. Returns null when no action of this
* shell's own is set.
*/
export async function runExitTrap(executeFn, session, status, stdin = null, callStack = null) {
const action = session.exitTrap;
if (executeFn === null ||
action === null ||
session.exitTrapInherited ||
session.trapStatus !== null) {
return null;
}
session.exitTrap = null;
if (action === '')
return null;
recordStatus(session, status);
const saved = session.trapStatus;
session.trapStatus = status;
const exiting = session.errexitExiting;
session.errexitExiting = false;
let final = status;
let stdout;
let stderr;
try {
const io = await executeFn(action, {
sessionId: session.sessionId,
session,
stdin,
callStack: callStack ?? new CallStack(session.shellNesting),
});
stdout = await materialize(io.stdout);
stderr = await io.materializeStderr();
if (io.exitCode !== 0 && session.shellOptions.errexit === true && !session.errexitImmune)
final = io.exitCode;
}
catch (err) {
if (err instanceof ExitSignal) {
stdout = err.stdout ?? new Uint8Array();
stderr = err.stderr;
final = err.exitCode;
}
else if (err instanceof ReturnSignal) {
stdout = await materialize(err.stdout);
stderr = err.stderr;
}
else {
throw err;
}
}
finally {
session.trapStatus = saved;
session.errexitExiting = exiting;
session.exitTrap = null;
session.exitTrapInherited = false;
}
recordStatus(session, final);
return new IOResult({ stdout, stderr: stderr.byteLength > 0 ? stderr : null, exitCode: final });
}
/**
* End a child shell whose line returned `io`: a `bash -c`, a script, a
* `$( )`. Its EXIT action runs after what it wrote.
*/
export async function finishShell(executeFn, session, io, stdin = null, callStack = null) {
const cleanup = await runExitTrap(executeFn, session, io.exitCode, stdin, callStack);
if (cleanup === null)
return io;
const stderr = concat([await io.materializeStderr(), await cleanup.materializeStderr()]);
return new IOResult({
stdout: asyncChain([io.stdout, cleanup.stdout]),
stderr: stderr.byteLength > 0 ? stderr : null,
exitCode: cleanup.exitCode,
refusal: io.refusal,
});
}
/**
* Run the whole of a child shell, a pipeline stage or a job, then its EXIT
* action. An `exit` or a fatal error that ends it carries the action's
* output and status on out to the boundary containing it.
*/
export async function endShell(executeFn, session, stdin, callStack, body) {
let stdout;
let io;
let execNode;
try {
;
[stdout, io, execNode] = await body;
}
catch (err) {
if (err instanceof ExitSignal) {
const cleanup = await runExitTrap(executeFn, session, err.containedCode, stdin, callStack);
if (cleanup !== null) {
err.stdout = concat([err.stdout ?? new Uint8Array(), await cleanup.materializeStdout()]);
err.stderr = concat([err.stderr, await cleanup.materializeStderr()]);
err.exitCode = err.containedCode = cleanup.exitCode;
}
}
else if (err instanceof ReturnSignal) {
const cleanup = await runExitTrap(executeFn, session, err.exitCode, stdin, callStack);
if (cleanup !== null)
throw new ReturnSignal(cleanup.exitCode, concat([err.stderr, await cleanup.materializeStderr()]), asyncChain([err.stdout, cleanup.stdout]));
}
throw err;
}
if (session.exitTrap === null || session.exitTrapInherited)
return [stdout, io, execNode];
stdout = await applyBarrier(stdout, io, BarrierPolicy.VALUE);
const cleanup = await runExitTrap(executeFn, session, io.exitCode, stdin, callStack);
if (cleanup === null)
return [stdout, io, execNode];
io = await io.merge(new IOResult({ stderr: cleanup.stderr }));
io.exitCode = cleanup.exitCode;
execNode.exitCode = cleanup.exitCode;
return [asyncChain([stdout, cleanup.stdout]), io, execNode];
}
//# sourceMappingURL=traps.js.map