@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
297 lines • 12.6 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 { chunks } from "../../../io/cooperative.js";
import { currentCaller } from "../../../context/session_context.js";
import { PathSpec } from "../../../types.js";
import { eloop, isEnoent, isEnotdir } from "../../../errors/fs.js";
import { CycleError, posixNormpath, resolvePath } from "../../../utils/path.js";
import { rstripSlash } from "../../../utils/slash.js";
import { operandsOf, readerOf } from "../../../workspace/files.js";
import { resolvePathStat } from "./paths.js";
/**
* Where a name stands as lstat reaches it: every link above its final name
* followed, the name itself kept.
*
* The walk the kernel gives a path before a call that does not follow its
* final name, and the key the table files a link under, since every link is
* keyed by its resolved parent. A path ending in a slash resolves whole:
* POSIX reads `dlink/` as `dlink/.`. Throws CycleError on a loop. Mirrors
* Python's link_location.
*/
export function linkLocation(links, virtual) {
const cut = virtual.lastIndexOf('/');
const name = virtual.slice(cut + 1);
if (name === '')
return links.resolve(virtual);
const head = virtual.slice(0, cut);
return `${links.resolve(head === '' ? '/' : head).replace(/\/+$/, '')}/${name}`;
}
/**
* Where the name an operand was typed as stands, as lstat reaches it.
*
* Every component but the last resolves through the links, as the kernel
* walks any path, so `dl/tl.gz` with `dl` a link to `dir` stands at
* `dir/tl.gz`: the table keys every link by that resolved parent. Null where
* the last component resolves as well, which is a name typed with a trailing
* slash or ending in a dot, and where the walk already failed. Mirrors
* Python's name_location.
*/
export function nameLocation(links, path, cwd) {
const typed = path.rawPath !== '' ? path.rawPath : path.virtual;
const last = typed.slice(typed.lastIndexOf('/') + 1);
if (path.walkError !== null || typed.endsWith('/') || last === '.' || last === '..')
return null;
return linkLocation(links, resolvePath(typed, cwd));
}
/**
* The link standing at the name an operand was typed as, its own row.
*
* The router follows an operand through its link before the command runs,
* which leaves `virtual` at the target and the name in `rawPath`; a command
* that acts on the name itself (cp -P, gzip's O_NOFOLLOW open) asks here.
* Null where no link stands at the name. Mirrors Python's typed_link.
*/
export function typedLink(links, path, cwd) {
const where = nameLocation(links, path, cwd);
return where === null ? null : links.statAt(where);
}
/**
* The namespace's links as a command meets a name, and the dispatcher past them.
*
* A link is invisible to every backend, so a command bound to one mount needs
* the links to tell a name that stands on one, and the dispatcher to act where
* the link leads or where it stands: the target may live on any mount, and so
* may the link. Mirrors Python's LinkResolver.
*/
export class LinkResolver {
links;
dispatch;
cwd;
constructor(links, dispatch, cwd) {
this.links = links;
this.dispatch = dispatch;
this.cwd = cwd;
}
/** Where the link standing at an operand's name sits, null where no link
* stands there. */
linkAt(path) {
const where = nameLocation(this.links, path, this.cwd);
if (where === null || this.links.statAt(where) === null)
return null;
return where;
}
/**
* Whether the link the router followed an operand through is gone.
*
* The router follows every operand before the command runs, so an earlier
* operand that removed the link (`gunzip -f l.gz l.gz`) leaves a later one
* at the old target: GNU opens each name when it reaches it, and finds
* nothing there.
*/
vanished(path) {
const where = nameLocation(this.links, path, this.cwd);
return where !== null && where !== path.virtual && this.links.statAt(where) === null;
}
/**
* Where a link leads, every link on the way followed. A chain that loops
* throws ELOOP, an error a walker reports as it reports any failed stat.
*/
target(link) {
return orEloop((path) => this.links.resolve(path), link);
}
/** What a name leads to, its stat through the dispatcher, on any mount. */
async stat(virtual) {
const [stat] = await this.dispatch('stat', PathSpec.fromStrPath(virtual));
return stat;
}
/** A directory's entries through the dispatcher, links among them. */
async readdir(virtual) {
const [entries] = await this.dispatch('readdir', PathSpec.fromStrPath(virtual));
return [...entries];
}
/** A name's own stat through the dispatcher: a link's, not its target's. */
async lstat(path) {
const [stat] = await this.dispatch('stat', path, [], { nofollow: true });
return stat;
}
/** What a name leads to, read through the dispatcher, which follows it. */
async *read(virtual) {
const [data] = await this.dispatch('read', PathSpec.fromStrPath(virtual), [], { stream: true });
yield* chunks(data);
}
/** Write a file on the mount that owns the name. */
async write(virtual, data) {
await this.dispatch('write', PathSpec.fromStrPath(virtual), [data]);
}
/** Remove the name itself, a link rather than what it leads to. */
async unlink(virtual) {
await this.dispatch('unlink', PathSpec.fromStrPath(virtual));
}
}
function orEloop(resolve, virtual) {
try {
return resolve(virtual);
}
catch (err) {
if (err instanceof CycleError)
throw eloop(virtual);
throw err;
}
}
/** Whether the mount at `prefix` is the one serving `virtual`; every path is
* where no mount table says otherwise. Mirrors Python's serves. */
export function serves(mounts, prefix, virtual) {
return mounts === undefined || mounts.rootOf(virtual).replace(/\/+$/, '') === prefix;
}
/**
* The mount a walked path lands on, every link on it followed. A mount is
* mirage's filesystem, so this stands for the device stat(2) reports, which
* `find -xdev` compares with the start point's: a link onto another mount
* leaves it as a mount root does (findutils 4.10). `virtual` is a directory
* the walk reached, which resolves. Mirrors Python's landing_root.
*/
export function landingRoot(mounts, links, virtual) {
return mounts.rootOf(links !== null ? links.resolve(virtual) : virtual);
}
/**
* The symlink facts a walk goes by, read off the session's files, or
* undefined outside a workspace.
*
* The dispatcher's reader answers (`NamespaceReader`), filtered by the rule
* it refuses a call by: a hidden link, or one that leads where the session
* cannot see, has no row, no listing entry and no target. Resolving a hidden
* path leaves it as typed, for the dispatcher to refuse; a visible one
* resolves, so a walker judges where it lands. Mirrors Python's link_view.
*/
export function linkView(files) {
if (files === undefined)
return undefined;
const seen = readerOf(files);
if (!seen.linked)
return undefined;
return {
statAt: (path) => seen.linkStatAt(path),
subtree: (directory) => seen.linkStatsBelow(directory),
resolve: (path) => posixNormpath(seen.follow(path)),
exists: async (path) => {
try {
return ((await resolvePathStat(files.dispatch, PathSpec.fromStrPath(path, undefined, '/'))) !==
null);
}
catch (err) {
console.debug(`link probe at ${path} failed: ${String(err)}`);
return false;
}
},
targetStat: async (path) => {
if (!seen.visible(path))
return null;
try {
return await files.stat(posixNormpath(seen.follow(path)));
}
catch (err) {
if (!(err instanceof CycleError) && !isEnoent(err) && !isEnotdir(err))
throw err;
console.debug(`link target of ${path} has no row: ${String(err)}`);
return null;
}
},
};
}
/**
* Where the mount boundaries are, read off the session's files, or undefined
* outside a workspace. Every mount below a path, hidden or not, is a boundary
* a backend's own walk stops at; only the visible ones are ever named. A walk
* through the dispatcher (`crossing`) has no boundary to stop at
* (`NamespaceReader.mounts`). Mirrors Python's mount_view.
*/
export function mountView(files, crossing = false, command, profile) {
if (files === undefined)
return undefined;
return readerOf(files).mounts(crossing, command, profile);
}
/**
* The names a directory holds only as namespace structure, as the session
* sees them: the mounts below it and its links, each by its first segment
* under the directory.
*
* No backend can list either, and the first may be one no backend serves at
* all (a link made below a directory chain that holds no key); these are the
* names the dispatcher merges into its own listing, from the same tables.
* Mirrors Python's owed_names.
*/
export function owedNames(files, directory) {
if (files === undefined)
return [];
return readerOf(files).owed(directory);
}
/** The workspace's links as the command's session sees them, read off its
* files; undefined outside a workspace. Mirrors Python's links_of. */
export function linksOf(ctx) {
return linkView(ctx.vfs);
}
/**
* The mount whose command scope the command runs in, null for none: the one
* its files were said to run in (`Files.servedBy`), so a read after an await
* answers for this run even where the async context does not isolate tasks;
* else the context's.
*/
function servingMount(files) {
return files.served !== undefined ? files.served : currentCaller().mount;
}
/**
* Whether the command walks across mounts: it runs on the session's files
* with no mount's command scope bound, as a cross-mount run does, so every
* call it makes reaches whichever mount serves the path. Mirrors Python's
* crosses_mounts.
*/
export function crossesMounts(ctx) {
return ctx.vfs !== undefined && servingMount(ctx.vfs) === null;
}
/**
* The prefix of the mount the command runs on, without its trailing slash:
* what a path the command makes up (an output name, a file a list names) is
* keyed below. '' for a run across mounts, which keys every path by its
* whole virtual path, and outside a workspace. Mirrors Python's mount_root.
*/
export function mountRoot(ctx) {
const mount = ctx.vfs === undefined ? null : servingMount(ctx.vfs);
return mount === null ? '' : rstripSlash(mount[0]);
}
/**
* Where the mount boundaries are for the command's walk, read off its files;
* undefined outside a workspace. A run across mounts (`crossesMounts`) has
* the dispatcher descend into every mount, so it has no boundary to stop at.
* Mirrors Python's mounts_of.
*/
export function mountsOf(ctx) {
return mountView(ctx.vfs, crossesMounts(ctx), ctx.spec?.name);
}
/** The link resolver an invocation carries: null outside a workspace.
* Mirrors Python's link_resolver. */
export function linkResolver(ctx) {
const links = linksOf(ctx);
if (links === undefined || ctx.vfs === undefined)
return null;
return new LinkResolver(links, ctx.vfs.dispatch, ctx.cwd.virtual);
}
/** The dispatcher a command's files run on. Mirrors Python's dispatch_of. */
export function dispatchOf(ctx) {
return ctx.vfs?.dispatch;
}
/** The line's operand resolution its files carry. Mirrors Python's follow_of. */
export function followOf(ctx) {
return ctx.vfs === undefined ? undefined : (operandsOf(ctx.vfs) ?? undefined);
}
//# sourceMappingURL=links.js.map