@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
455 lines • 21.3 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 { currentCaller, sessionVisibility } from "../../../context/session_context.js";
import { eacces, eisdir, enoent, isDotWalkError, isEnoent } from "../../../errors/fs.js";
import { ensureStream } from "../../../io/stream.js";
import { revisionFor } from "../../../observe/context.js";
import { FileStat, FileType } from "../../../types.js";
import { hasAborted, makeAbortError } from "../../../utils/abort.js";
import { makeResolveGlob } from "../../../utils/glob_walk.js";
import { moveReveals } from "../../../utils/hidden.js";
import { CycleError } from "../../../utils/path.js";
import { guardInput } from "../utils/limit.js";
import { crossesMounts, followOf, linkLocation, linksOf, mountRoot, mountView, mountsOf, owedNames, } from "../utils/links.js";
import { isImplicitDir } from "../utils/paths.js";
import { callerStream } from "./dispatch.js";
const READINGS = new WeakMap();
/**
* `ctx` as the factory hands it to a builder, its files read as `reading`
* says and under the command tier's guards. Mirrors Python's `with_reading`.
*/
export function withReading(ctx, reading) {
const bound = { ...ctx };
READINGS.set(bound, reading);
return bound;
}
// The writes a run across mounts makes at the dispatcher, whichever mount
// serves the path; of the other functions it takes only `readTree`'s
// shortcut, where no mount sits below the subtree.
const CROSSING_CALLS = new Set([
'write',
'pwrite',
'unlink',
'mkdir',
'truncate',
]);
// How many operands a whole read warms ahead of itself (`readAhead`).
const READ_AHEAD = 8;
/**
* What a command's whole read of an operand awaits first, and what learns the
* operands its globs resolve to.
*
* A command reads its operands in turn, each whole once the one before is
* done. On a whole read of one, the next READ_AHEAD go to the dispatcher as
* one `read_many`, which keeps the small files among them in the read cache
* (one request where the store reads a batch), and the batch after goes once
* half of that one is read. A read whose file is still being warmed waits for
* it, so it is served warm instead of fetching the file again. No bytes are
* held here: a write in between drops the cached copy as it drops any. A
* path that is not an operand warms nothing. Mirrors Python's `read_ahead`.
*/
export function readAhead(dispatch, paths) {
const operands = [];
const order = new Map();
const warming = new Map();
let sent = 1;
const noted = (found) => {
for (const path of found) {
if (path.pattern === null && !order.has(path.virtual)) {
order.set(path.virtual, operands.length);
operands.push(path);
}
}
};
const warmed = async (path) => {
const at = order.get(path.virtual);
if (at === undefined)
return;
const start = Math.max(sent, at + 1);
if (start < operands.length && start <= at + READ_AHEAD / 2) {
const batch = operands.slice(start, start + READ_AHEAD);
sent = start + batch.length;
const task = warm(dispatch, batch);
for (const p of batch)
warming.set(p.virtual, task);
}
await warming.get(path.virtual);
};
noted(paths);
return [warmed, noted];
}
/**
* Ask the dispatcher to keep `paths` in the read cache. A failure is logged
* and dropped: each read that follows meets it. Mirrors Python's `_warm`.
*/
async function warm(dispatch, paths) {
const [first, ...rest] = paths;
if (first === undefined)
return;
try {
await dispatch('read_many', first, [], { paths: rest });
}
catch (err) {
console.debug(`read-ahead from ${first.virtual} failed: ${String(err)}`);
}
}
/**
* Send one call to the dispatcher and hand back its answer: the one way a
* command's file call, a cross-mount run and a relayed command reach a mount.
* Mirrors Python's `dispatched_call`.
*/
export async function dispatchedCall(dispatch, name, path, args = [], kwargs = {}) {
const [result] = await dispatch(name, path, args, kwargs);
return result;
}
/**
* The `readTree` a walk through the dispatcher takes, asked only where the
* mount a subtree lands on answers one; elsewhere null, and the walk lists
* instead.
*
* The serving mount's answer is no guide: a walk through the dispatcher also
* reaches the mounts below it and those a link above the subtree leads to, so
* a subtree on one that answers no read_tree (a disk mount under the root RAM)
* is walked rather than refused with ENOTSUP. A walk across mounts
* (`crossing`) asks only where no mount sits below the subtree either: the
* serving backend's answer holds keys a mount there shadows, and lacks the
* mount's own. A subtree read stops at a link at the final name (ENOTDIR), so
* a walk that follows links lists them directory by directory instead. One
* level (`level`) is the listing a `readdir` answers with each child's stat,
* which every mount that keeps listings answers, asked only where no mount
* sits below, whose root the listing would name.
* `mounts` is the boundary view a walk that does not cross them sees
* (`mountView(files)`), undefined to take no shortcut; `landing` where a path lands, every link above its
* final name followed (`linkLocation`, `NamespaceLinks.followParent`), null
* for a walk whose paths name their own place. Mirrors Python's
* `dispatched_read_tree`.
*/
export function dispatchedReadTree(dispatch, mounts, landing, crossing) {
return async (path, limit, level = false) => {
let at;
try {
at = landing === null ? path.virtual : landing(path.virtual);
}
catch (err) {
if (!(err instanceof CycleError))
throw err;
console.debug(`following the links above ${path.virtual} failed: ${String(err)}`);
return null;
}
if (mounts === undefined ||
(!level && !mounts.answers(at, 'read_tree')) ||
((crossing || level) && mounts.descendants(at).length > 0))
return null;
return (await dispatchedCall(dispatch, 'read_tree', path, [], {
nofollow: true,
...(limit === undefined ? {} : { limit }),
...(level ? { level: true } : {}),
}));
};
}
/** A command's streamed read of the stored bytes, or a window of them, at the
* dispatcher, opened at the first pull as a backend stream is. */
async function* dispatchedStream(dispatch, path, ahead, offset = 0, size = null) {
if (ahead !== undefined)
await ahead(path);
const [data] = await dispatch('read', path, [], { stream: true, filetype: null, offset, size });
yield* ensureStream(data);
}
/**
* A command's stat of the entry at `path`, at the dispatcher: the entry
* itself, as lstat answers, so a link at the final name is the link and a
* trailing slash reaches what it names. The dispatcher merges the
* namespace's overlay into the row. Mirrors Python's `_dispatched_stat`.
*/
async function dispatchedStat(dispatch, path) {
const [found] = await dispatch('stat', path, [], { nofollow: true });
if (!(found instanceof FileStat))
throw enoent(path);
return found;
}
/**
* Whether an entry is at `path`, as `dispatchedStat` answers. Mirrors
* Python's `_dispatched_exists`.
*/
async function dispatchedExists(dispatch, path) {
try {
await dispatchedStat(dispatch, path);
}
catch (err) {
if (!isEnoent(err))
throw err;
console.debug(`nothing at ${path.virtual}: ${String(err)}`);
return false;
}
return true;
}
/** `fn`, refused with the line's abort instead of started once `signal` has fired. */
function refusedAfterAbort(signal, fn) {
if (signal === undefined)
return fn;
return (...args) => (hasAborted(signal) ? Promise.reject(makeAbortError(signal)) : fn(...args));
}
// Whether a path no backend knows is a directory the namespace owns: the
// third way a read operand can be a directory, after the explicit stat row
// and the implicit keyed-backend prefix. A directory that exists only
// because a mount or a link sits under it (`/repos` when `/repos/alpha` is
// mounted) belongs to no backend at all, so the mount this command runs on
// can neither stat it nor list it, and every read command reported it
// missing while stat, file, ls, du, find and tree all called it a directory.
//
// The names the namespace owes the path, not a dispatched stat. Both answer
// for a mount parent, but a dispatched stat also answers from a backend's own
// listing, and a backend that answers a path it does not hold with entries
// rather than a miss turns every such path into a directory: postgres reads
// any first segment as a schema and lists `tables` and `views` under it, so
// `cat /pg/nope.txt` refused a directory that is not there. The namespace
// cannot over-claim that way, because it derives a segment only from a mount
// prefix or a link path it actually holds, and it is the same authority
// `namespaceListing` gates on, so the listing and this refusal cannot
// disagree. It is hide-filtered for free, which is what keeps the parent of a
// mount the session may not be told about reading as absence.
function isNamespaceDir(ctx, p) {
return owedNames(ctx.vfs, p.virtual).length > 0;
}
// The one place the read family decides what a directory is, shared by the
// stat and the stream chokepoints so the two cannot drift.
async function statRefusingDirs(files, ctx, p) {
let st;
try {
st = await files.stat(p);
}
catch (e) {
if (e.code !== 'ENOENT' || isDotWalkError(e))
throw e;
if (await isImplicitDir(files.readdir, p))
throw eisdir(p);
if (isNamespaceDir(ctx, p))
throw eisdir(p);
throw e;
}
if (st.type === FileType.DIRECTORY)
throw eisdir(p);
return st;
}
// Stat for the read-family chokepoint (`splitReadable`): a directory operand
// fails with EISDIR instead of succeeding (explicit, via the stat type) or
// failing with ENOENT (implicit keyed-backend directory via a readdir probe,
// or a namespace-only mount parent via the name plane), so cat/head/tail
// report GNU's `Is a directory` and keep the remaining operands (#457).
// Mirrors Python's `dir_aware_stat`.
export function dirAwareStat(files, ctx) {
return (p) => statRefusingDirs(files, ctx, p);
}
/**
* A regular file's size from its stat, for a command whose answer is the size
* alone (`wc -c`), as GNU takes it from `st_size`. Null reads the file
* instead: for a file the read cache holds, which is read from there as the
* read it replaces would be; for anything but a regular file (a device or a
* pipe has no size to trust); for a size the stat does not know; for a path
* pinned to an older revision, whose size the stat does not give; and for a
* store that keeps permission bits (a disk, SFTP), where a file the stat sees
* may still refuse its read, and a disk's named pipe stats as a file. A store
* without them grants the read with the stat. A directory fails with EISDIR
* as `dirAwareStat` has it. A listing the index holds can be up to its TTL
* old, so the size is checked against a stat that asks the backend now
* (`fresh`), and a file whose size moved since the listing is read: whether
* that read serves the listed version (a GitHub blob the listing named) or the
* current one, it is what the command counts. Mirrors Python's `file_size`.
*/
export function fileSize(files, ctx, opened = false) {
return async (p) => {
if (READINGS.get(ctx)?.stream != null || (opened && crossesMounts(ctx)))
return null;
if (revisionFor(p.virtual) !== null)
return null;
if (await files.isWarm(p))
return null;
const st = await statRefusingDirs(files, ctx, p);
if (st.type !== FileType.FILE ||
st.size === null ||
(st.mode !== null && (!opened || st.size === 0)))
return null;
if (ctx.vfs === undefined)
return st.size;
const [now] = await ctx.vfs.dispatch('stat', p, [], { fresh: true });
return now instanceof FileStat && now.size === st.size ? st.size : null;
};
}
async function* streamRefusingDirs(files, ctx, p) {
await statRefusingDirs(files, ctx, p);
yield* files.read(p);
}
// The per-operand read stream, `read(path)`, the read family takes:
// the operand is stat'ed first so a directory fails with EISDIR before any
// backend read runs (sftp reads of a directory raise an opaque `Failure`,
// not ENOENT), and an ENOENT for an implicit keyed-backend directory or a
// namespace-only mount parent is refined the same way `dirAwareStat` does,
// before the generic formats the stderr line (#457). Mirrors Python's
// `dir_aware_stream`.
export function dirAwareStream(files, ctx) {
return (p) => streamRefusingDirs(files, ctx, p);
}
/**
* Expand glob operands, or [] when there is nothing to resolve: no operand
* resolves to no paths, which the generics read as stdin mode. Operand
* semantics (report-and-continue, directory refusal) live in the generics
* via `splitReadable`; this is wiring only. Mirrors Python's
* `resolve_or_empty`.
*/
export async function resolveOrEmpty(files, paths) {
return paths.length > 0 ? files.resolveGlob(paths) : [];
}
/** Refuse a relocation that would surface a hidden path.
*
* A rename or a native directory copy re-anchors everything below its
* source, and a hide's coverage does not move with the content, so
* hidden bytes would land at paths the session can see. EACCES on the
* source, which mv and cp render in GNU's permission-denied voice.
* Only a directory has anything below it to re-anchor, so callers
* check this for a source they know is a directory and skip it for a
* file. Mirrors Python's `refuse_reveal`. */
export function refuseReveal(src, dst, access) {
if (moveReveals(sessionVisibility(access), src.virtual, dst.virtual))
throw eacces(src.virtual);
}
/**
* The files of the command `ctx` runs, its operands `paths`.
*
* Every call goes to the dispatcher through `ctx.vfs`, which refuses a read
* of a directory and a write to a slash-terminated name and fills a stat's
* unknown size from the read cache. A command on one mount reads the stored
* bytes (`filetype: null`), its whole reads warmed ahead of themselves
* (`readAhead`), and, as the factory hands it on (`withReading`), under the
* command tier's guards: no call started once the line's signal has fired,
* each operand's read cut into pieces under its abort and deadline. A run
* across mounts (`crossesMounts`) reads each operand as the
* command that asked for it, through the path's renderer, after a stat that
* refuses a directory, and lists with no boundary to stop at. Throws without
* `ctx.vfs`: a builder outside a workspace has no files to read. Mirrors
* Python's `command_files`.
*/
export function commandFiles(ctx, paths = []) {
const vfs = ctx.vfs;
if (vfs === undefined)
throw new TypeError(`${ctx.spec?.name ?? ''}: no workspace files`);
const dispatch = vfs.dispatch;
const crossing = crossesMounts(ctx);
const reading = crossing ? undefined : READINGS.get(ctx);
const signal = reading === undefined ? undefined : ctx.signal;
// What the mount can do is the serving mount's to say: an option can route
// the command to it with no operand there (`shuf -i 1-1 -o /ram/out` typed
// on another mount).
const home = mountRoot(ctx);
const anchor = `${home}/`;
const mounts = mountsOf(ctx);
const [ahead, noted] = readAhead(dispatch, paths);
const call = (name, path, args = [], kwargs = {}) => dispatchedCall(dispatch, name, path, args, kwargs);
const guard = (fn) => refusedAfterAbort(signal, fn);
let rawStat;
let rawReaddir;
let rawStream;
if (crossing) {
rawStat = async (path) => (await dispatch('stat', path, [], { nofollow: true }))[0];
rawReaddir = async (path, _nofollow, limit) => (await dispatch('readdir', path, [], limit === undefined ? {} : { limit }))[0];
rawStream = (path) => callerStream(dispatch, currentCaller(), path, ahead);
}
else {
rawStat = (path) => dispatchedStat(dispatch, path);
rawReaddir = async (path, nofollow, limit) => (await call('readdir', path, [], {
...(nofollow === true ? { nofollow } : {}),
...(limit === undefined ? {} : { limit }),
}));
const stream = reading?.stream ?? null;
rawStream =
stream === null
? (path) => dispatchedStream(dispatch, path, ahead)
: (path) => stream(ctx, path) ?? dispatchedStream(dispatch, path, ahead);
}
const read = (path, offset = 0, size = null) => {
const source = offset === 0 && size === null
? rawStream(path)
: dispatchedStream(dispatch, path, undefined, offset, size);
return reading === undefined ? source : guardInput(source, ctx, reading.split);
};
// A run across mounts runs in no mount's cache scope: it takes an operand's
// size from its stat, warm or not.
const isWarm = async (path) => crossing ? false : (await call('read', path, [], { filetype: null, warm: true })) === true;
const stat = guard(rawStat);
const readdir = guard(rawReaddir);
const table = linksOf(ctx);
const links = crossing ? undefined : table;
const access = crossing ? undefined : ctx.session?.access();
return {
...(ctx.signal === undefined ? {} : { signal: ctx.signal }),
stat,
exists: guard((path) => dispatchedExists(dispatch, path)),
readdir,
read,
isWarm: guard(isWarm),
write: guard(async (path, data) => {
await call('write', path, [data]);
}),
append: guard(async (path, data) => {
await call('append', path, [data]);
}),
pwrite: guard(async (path, data, offset) => {
await call('pwrite', path, [data, offset]);
}),
mkdir: guard(async (path, parents) => {
await call('mkdir', path, [], { parents: parents ?? false });
}),
unlink: guard(async (path) => {
await call('unlink', path);
}),
rmdir: guard(async (path) => {
await call('rmdir', path);
}),
rmR: guard(async (path) => {
await call('rm_r', path);
}),
rename: guard(async (src, dst) => {
await call('rename', src, [dst]);
}),
copy: guard(async (src, dst) => {
await call('copy', src, [dst]);
}),
dirCopy: guard(async (src, dst) => {
await call('dir_copy', src, [dst]);
}),
truncate: guard(async (path, length, noCreate) => {
await call('truncate', path, [length], { no_create: noCreate ?? false });
}),
setAttrs: guard(async (path, fields) => (await call('setattr', path, [], { ...fields }))),
firstLines: guard(async (path, n) => (await call('first_lines', path, [n]))),
lastLines: guard(async (path, n) => (await call('last_lines', path, [n]))),
lineCount: guard(async (path) => (await call('line_count', path))),
checksum: guard(async (path, algorithm) => (await call('checksum', path, [algorithm]))),
follow: guard(async (path, n, interval) => (await call('follow', path, [n, interval]))),
readTree: guard(dispatchedReadTree(dispatch, mountView(vfs), table === undefined ? null : (virtual) => linkLocation(table, virtual), crossing)),
filesContaining: async (path, text, opts) => (await call('files_containing', path, [text], {
whole_word: opts.wholeWord,
ignore_case: opts.ignoreCase,
})),
linesContaining: async (path, text, opts) => (await call('lines_containing', path, [text], {
ignore_case: opts.ignoreCase,
})),
resolveGlob: makeResolveGlob(readdir, mounts?.walkLimit?.(anchor) ?? -1, stat, links === undefined ? undefined : (virtual) => links.targetStat(virtual), access, followOf(ctx), noted),
answers: (name) => name === 'read_range' && reading?.stream != null
? false
: crossing
? CROSSING_CALLS.has(name) || name === 'read_tree'
: (mounts?.answers(anchor, name) ?? false),
};
}
//# sourceMappingURL=files.js.map