@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
123 lines • 5.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 { resolvePathStat } from "../../../../commands/builtin/utils/paths.js";
import { eisdir, fsStrerror } from "../../../../errors/fs.js";
import { IOResult, materialize } from "../../../../io/types.js";
import { decodeText, encodeText } from "../../../../shell/bytes.js";
import { CANNOT_EXECUTE_STATUS, NOT_FOUND_STATUS } from "../../../../shell/constants.js";
import { FileType } from "../../../../types.js";
import { resolvePath } from "../../../../utils/path.js";
import { ExecutionNode } from "../../../types.js";
import { toScope } from "../scope.js";
/**
* A diagnostic from a shell that never got as far as running.
*
* `prefix` and `command` come apart because bash reports itself by
* `argv[0]`, which for a script operand is the operand: the recorded
* command still has to be the builtin that ran, not a file path.
*/
export function scriptError(prefix, message, code, command) {
const err = encodeText(`${prefix}: ${message}\n`);
return [
null,
new IOResult({ exitCode: code, stderr: err }),
new ExecutionNode({ command: command ?? prefix, exitCode: code, stderr: err }),
];
}
/**
* Read a script file through the op dispatcher.
*
* Every way of running a script off a mount comes through here, so a backend
* quirk is answered once rather than per caller. The one answered today is a
* directory, which only a real filesystem reports as EISDIR on read: a keyed
* backend has no directory object to open and answers ENOENT, ssh's raw error
* carries no errno, and WebDAV serves the collection's HTML listing as bytes,
* which a loader that read first would then run as a script. So the stat
* probe runs before the read, and asks both channels a backend can answer on,
* since on a prefix store a directory is the set of keys under it rather than
* an object. A stat miss alone proves nothing (absence takes two channels), so
* only a positive directory answer is acted on and the read still owns "no
* such file".
*
* The caller owns the diagnostic: `source` and a nested shell word the same
* failure differently and exit differently on it.
*/
export async function readScriptBytes(dispatch, path, cwd) {
const scope = toScope(resolvePath(path, cwd));
const stat = await resolvePathStat(dispatch, scope);
if (stat !== null && stat.type === FileType.DIRECTORY)
throw eisdir(path);
const [data] = await dispatch('read', scope);
if (data === null || data === undefined)
return new Uint8Array();
return materialize(data);
}
/** Read a script file as text, a byte UTF-8 cannot read replaced. */
export async function readScriptText(dispatch, path, cwd) {
return decodeText(await readScriptBytes(dispatch, path, cwd));
}
export function isBinary(data) {
// Bash checks ELF magic and NUL before the first newline in its
// initial 80-byte sample, not whether the entire file is UTF-8.
if (data[0] === 0x7f && data[1] === 0x45 && data[2] === 0x4c && data[3] === 0x46)
return true;
for (const byte of data.subarray(0, 80)) {
if (byte === 0x0a)
break;
if (byte === 0)
return true;
}
return false;
}
/**
* Read a script file operand, or the failure bash reports for it.
*
* GNU splits the diagnostics by how far startup got, and both halves fall out
* of the errno rather than being listed case by case. A file the shell cannot
* open at all is blamed on the shell, and only a missing one is exit 127
* (`bash: run.sh: No such file or directory`); anything it found but could not
* run is 126 (`Permission denied`, `Not a directory`). A directory is the
* exception, because it opens fine and fails on the first read, by which point
* `$0` is already the operand, so bash prints it twice (`/tmp: /tmp: Is a
* directory`, exit 126). Reproduced rather than tidied up: it is what an agent
* copying a message into a search box will find.
*/
export async function readScriptFile(dispatch, name, path, session) {
try {
const data = await readScriptBytes(dispatch, path, session.cwd);
if (isBinary(data)) {
return [
null,
scriptError(path, `${path}: cannot execute binary file`, CANNOT_EXECUTE_STATUS, name),
];
}
return [decodeText(data), null];
}
catch (exc) {
// A strerror is exactly what makes this a filesystem error, so the
// lookup is both the test and the message.
const strerror = fsStrerror(exc);
if (strerror === null)
throw exc;
const code = exc.code;
if (code === 'EISDIR') {
return [null, scriptError(path, `${path}: ${strerror}`, CANNOT_EXECUTE_STATUS, name)];
}
return [
null,
scriptError(name, `${path}: ${strerror}`, code === 'ENOENT' ? NOT_FOUND_STATUS : CANNOT_EXECUTE_STATUS),
];
}
}
//# sourceMappingURL=script.js.map