@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
117 lines • 5.14 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 { CommandTimeoutError } from "../../../errors/types.js";
import { hasAborted } from "../../../utils/abort.js";
import { runOutput } from "../../../commands/builtin/general/interpreter.js";
import { EXTERNAL_COMMANDS } from "../../../runtime/constants.js";
import { isProcessExecutor } from "../../../runtime/mixin.js";
import { shellJoin } from "../../../shell/join.js";
import { PathSpec } from "../../../types.js";
import { envSnapshot } from "../../session/state.js";
import { ExecutionNode } from "../../types.js";
import { admissionDenial } from "./run.js";
import { encodeText } from "../../../shell/bytes.js";
import { concat } from "../../../utils/bytes.js";
import { Channel } from "../../../shell/console/index.js";
import { CANNOT_EXECUTE_STATUS } from "../../../shell/constants.js";
/**
* Execute one admitted program; its surrounding shell stays in Mirage.
*
* Its output is handed over as it runs (`runOutput`), within the line's
* deadline, kept alive until the output is closed. A program writes its
* stderr into `sink` as it arrives, where the statement it runs in writes (a
* job's console, a redirect, a compound body), so it lands beside its stdout
* and stays when the program is stopped; without one it lands on the result,
* kept as it arrives all the same.
*/
export async function runExternal(argv, stdin, session, registry, routing, signal, sink) {
const bindings = routing.bindings;
const key = Object.hasOwn(bindings, argv.name) ? argv.name : EXTERNAL_COMMANDS;
const runtime = bindings[key];
const command = shellJoin(argv.tokens);
if (runtime == null) {
const denied = admissionDenial(argv.name);
const stderr = await denied.materializeStderr();
return [null, denied, new ExecutionNode({ command, exitCode: CANNOT_EXECUTE_STATUS, stderr })];
}
// A runtime that runs an argv gets one; a line runtime gets the program
// as one quoted line.
const process = isProcessExecutor(runtime);
const kind = process ? 'process' : 'line';
const ctx = {
stdin,
flags: process ? {} : { c: command },
cwd: PathSpec.fromStrPath(session.cwd),
env: envSnapshot(session),
};
const node = new ExecutionNode({ command, exitCode: 0 });
const said = [];
const toSink = async (chunk) => {
await sink?.emit(Channel.STDERR, chunk);
};
const run = async (output, errors, stop) => {
// The command's record keeps a copy for the policies that read it.
const told = async (chunk) => {
said.push(new Uint8Array(chunk));
await errors(chunk);
};
try {
const result = await executeProgram(runtime, argv.name, process ? argv.tokens : [], { ...ctx, signal: stop }, { kind, output, errors: told });
node.stderr = concat([...said, result.stderr ?? new Uint8Array()]);
return result;
}
finally {
await registry.settle(runtime);
}
};
const [stdout, io] = runOutput(run, {
command: argv.name,
declared: null,
prefixes: registry.allMounts().map((mount) => mount.prefix),
native: true,
}, sink !== undefined ? toSink : undefined, stdin, signal);
// The record takes the status the output settles with: 141 for a reader
// that stopped early.
io.output?.callbacks.push(() => {
node.exitCode = io.exitCode;
});
return [stdout, io, node];
}
/**
* Keep an ordinary launch failure within the command that caused it.
*
* @param runtime the runtime the line routed to.
* @param name the program's name, for a launch failure's message.
* @param texts the argv, or nothing for a line.
* @param ctx the run's context.
* @param opts a process or a line, and where a streamed process writes its
* stdout and its stderr.
*/
async function executeProgram(runtime, name, texts, ctx, opts) {
try {
return await runtime.execute([], texts, ctx, opts);
}
catch (error) {
if (error instanceof CommandTimeoutError ||
(error instanceof Error && error.name === 'AbortError') ||
hasAborted(ctx.signal))
throw error;
return {
stdout: new Uint8Array(),
stderr: encodeText(`${name}: ${error instanceof Error ? error.message : String(error)}\n`),
exitCode: 1,
};
}
}
//# sourceMappingURL=external.js.map