@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
297 lines • 13 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 } from "../../context/session_context.js";
import { walkBound } from "../../policy/builtin/output_cap.js";
import { Limit, PathSpec } from "../../types.js";
import { hiddenUnder, leadsHidden, pathVisible } from "../../utils/hidden.js";
import { norm } from "../../utils/path.js";
import { normDir, ownerPrefix, rstripSlash } from "../../utils/slash.js";
import { compareCodePoints } from "../../utils/sort.js";
import { namespaceNames } from "../../view/namespace_view.js";
/**
* Whether a session sees a path: it is not hidden, and no link on it leads
* where the session cannot see.
*
* The rule the dispatcher refuses a call by, and the one every answer a
* `NamespaceReader` gives is filtered by. Mirrors Python's `sees`.
*/
export function sees(vis, links, virtual) {
if (!pathVisible(vis, virtual))
return false;
if (vis === null || links === null)
return true;
return !leadsHidden(vis, virtual, links.symlinkTargets());
}
/**
* What one session sees of the namespace: its links and its mounts.
*
* The namespace the dispatcher answers from owns the link table and the mount
* table and hands this out (`Namespace.readers`); it is the one way anything
* above the dispatcher reads them. Every answer is filtered by the rule the
* dispatcher refuses a call by (`sees`): a hidden link, or one that leads
* where the session cannot see, has no row, no listing entry and no target,
* and a hidden mount is a boundary a walk stops at but is never named. It is a
* link table itself (`NamespaceLinks`), the namespace's as the session sees
* it, so whatever takes a link table above the dispatcher is handed this one.
* Live, so a link made or removed mid-command is seen by the next question,
* and synchronous, so a walk asks per entry at no dispatch cost. Mirrors
* Python's `NamespaceReader`.
*/
export class NamespaceReader {
links;
allRows;
sessionId;
visibility;
/**
* @param links the namespace's link table, null where the files stand
* outside a workspace.
* @param allRows every registered mount.
* @param sessionId the session the answers are for, which decides the
* mounts it is told of at all.
* @param visibility the visibility of that session, asked when a question is.
*/
constructor(links, allRows, sessionId, visibility) {
this.links = links;
this.allRows = allRows;
this.sessionId = sessionId;
this.visibility = visibility;
}
/** Whether a link table stands behind the answers. */
get linked() {
return this.links !== null;
}
/** Whether the session sees `path` (`sees`). */
visible(path) {
const virtual = PathSpec.fromStrPath(path, undefined, '/').virtual;
return sees(this.visibility(), this.links, virtual);
}
/**
* Whether the session could hide anything at or under `path`, so a reader
* of a store of its own (a repository's history, say) has to ask `visible`
* of what it finds there.
*/
hidesUnder(path) {
const virtual = PathSpec.fromStrPath(path, undefined, '/').virtual;
const vis = this.visibility();
if (hiddenUnder(vis, virtual))
return true;
if (this.links === null || !hiddenUnder(vis, '/'))
return false;
// `visible` follows links, so a link at, under or above the path can lead
// where the session hides even when nothing named under the path is hidden.
const below = `${rstripSlash(virtual)}/`;
return [...this.links.symlinkTargets().keys()].some((link) => link === virtual || link.startsWith(below) || virtual.startsWith(`${rstripSlash(link)}/`));
}
/**
* Every registered mount, hides and sessions aside, longest prefix first:
* what a facade's record asks of the mount table.
*/
rows() {
return [...this.allRows()].sort((a, b) => b.prefix.length - a.prefix.length);
}
/** The registered mount owning `path` by longest prefix. */
owner(path) {
const rows = this.rows();
const found = ownerPrefix(rows.map((m) => m.prefix), path);
return rows.find((m) => m.prefix === found) ?? null;
}
/**
* The mounts the session is told of at all, hides aside. A session's own
* documents mount is one only it is told of.
*/
served() {
return this.rows().filter((m) => m.visible === undefined || m.visible === null || m.visible(this.sessionId));
}
/**
* The mounts whose root the session sees. A hidden mount is left out of
* every list here, as it is left out of a listing, so no list names it.
*/
shown() {
const vis = this.visibility();
return this.served().filter((m) => pathVisible(vis, norm(m.prefix)));
}
/**
* The prefix of the mount serving `path`, without a trailing slash; null
* for a path the session cannot see.
*/
mountOf(path) {
const virtual = PathSpec.fromStrPath(path, undefined, '/').virtual;
if (!pathVisible(this.visibility(), virtual))
return null;
const owner = ownerPrefix(this.served().map((m) => m.prefix), virtual);
return owner === null ? null : norm(owner);
}
/** Whether `path` is the root of a mount the session sees. */
isMount(path) {
const virtual = norm(PathSpec.fromStrPath(path, undefined, '/').virtual);
return this.shown().some((m) => norm(m.prefix) === virtual);
}
/**
* The mount roots strictly under `path`, sorted, without a trailing slash;
* a hidden one reported by the nearest folder above it the session sees.
*/
submounts(path) {
const virtual = norm(PathSpec.fromStrPath(path, undefined, '/').virtual);
const vis = this.visibility();
if (!pathVisible(vis, virtual))
return [];
const below = normDir(virtual);
const found = new Set();
for (const m of this.served()) {
let root = norm(m.prefix);
if (root === virtual || !normDir(root).startsWith(below))
continue;
while (root !== virtual && !pathVisible(vis, root)) {
root = root.slice(0, root.lastIndexOf('/')) || '/';
}
found.add(root);
}
return [...found].sort(compareCodePoints);
}
/** Whether a link the session sees stands at `path`. */
isLink(path) {
return this.links !== null && this.visible(path) && this.links.isLink(path);
}
/**
* The link standing at a path, its own row; null where none stands or the
* session cannot see it.
*/
linkStatAt(path) {
if (this.links === null || !this.visible(path))
return null;
return this.links.linkStatAt(path);
}
/**
* Every link the session sees, to its stored target. A session that hides
* nothing sees the whole table, which is asked of no link.
*/
symlinkTargets() {
if (this.links === null)
return new Map();
const targets = this.links.symlinkTargets();
const vis = this.visibility();
if (vis === null || !hiddenUnder(vis, '/'))
return targets;
return new Map([...targets].filter(([path]) => this.visible(path)));
}
/** The links at any depth under a directory the session sees, each with its own row. */
linkStatsBelow(directory) {
if (this.links === null)
return [];
return this.links.linkStatsBelow(directory).filter(([path]) => this.visible(path));
}
/**
* The names of the links directly under a directory that the session sees,
* which a listing's link marks need.
*/
linkNamesUnder(directory) {
if (this.links === null)
return new Set();
const base = directory === '/' ? '/' : `${rstripSlash(directory)}/`;
return new Set([...this.links.linkNamesUnder(directory)].filter((name) => this.visible(base + name)));
}
/**
* A link's stored target, as typed; null where no link stands or the
* session cannot see it.
*/
readlink(path) {
if (this.links === null || !this.visible(path))
return null;
return this.links.readlink(path);
}
/**
* Where a path the session sees leads, every link followed; one it cannot
* see, a link on it leading through a hide included, is left as typed, for
* the dispatcher to refuse. Throws CycleError where a link loop stands in
* the walk.
*/
follow(path) {
if (this.links === null || !this.visible(path))
return path;
return this.links.follow(path);
}
/**
* A path with every link above its final name followed, the walk a call
* that acts on the name itself is given; a path the session cannot see is
* left as typed.
* Throws CycleError where a link loop stands in the walk.
*/
followParent(path) {
if (this.links === null || !this.visible(path))
return path;
return this.links.followParent(path);
}
/**
* The names a directory holds only as namespace structure: the mounts
* below it and its links, by their first segment under it.
*/
owed(directory) {
return namespaceNames(this.visibility(), this.served().map((m) => m.prefix), this.links, directory);
}
/**
* Where the mount boundaries are, as the session sees them.
*
* Every mount below a path, hidden or not, is a boundary a backend's own
* walk stops at: a mount the session cannot see still shadows the parent
* backend's keys under its prefix. Only the visible ones are ever named. A
* walk through the dispatcher (`crossing`) descends into each mount itself,
* so it has no boundary to stop at: no mount below a path, and no mount
* root; and it takes no mount's shortcut, so no mount answers a function
* for it or narrows its search. A walk's `walkLimit` on each mount is
* `command`'s (walkBound), resolved through the session's `profile`, the
* mount's and the workspace's command limits and the running command's
* declared one; without a command, the one every command shares. Both
* default to the running command's and its session's.
*/
mounts(crossing, command = currentCaller().command, profile = currentCaller().session?.commandLimits ?? {}) {
const caller = currentCaller();
const declared = command === caller.command ? caller.declared : null;
const below = (path) => {
if (crossing)
return [];
const base = normDir(path);
return this.served()
.map((m) => m.prefix)
.filter((prefix) => prefix !== base && prefix.startsWith(base))
.sort(compareCodePoints)
.map((prefix) => rstripSlash(prefix) || '/');
};
const owner = (path) => {
const served = this.served();
const found = ownerPrefix(served.map((m) => m.prefix), path);
return served.find((m) => m.prefix === found) ?? null;
};
return {
descendants: below,
visibleDescendants: (path) => {
const vis = this.visibility();
return below(path).filter((root) => pathVisible(vis, root));
},
isRoot: (path) => !crossing && this.served().some((m) => m.prefix === normDir(path)),
rootOf: (path) => owner(path)?.prefix ?? '/',
answers: (path, name) => !crossing && (owner(path)?.calls?.has(name) ?? false),
searchable: (path) => (crossing ? null : (owner(path)?.searchable ?? null)),
walkLimit: (path) => {
const row = owner(path);
if (row === null)
return -1;
return walkBound({
commandLimits: new Map(row.commandLimits ?? []),
defaultLimit: new Limit({ walkLimit: row.walkLimit ?? -1 }),
}, command, declared, row.workspaceLimits ?? {}, profile);
},
};
}
}
//# sourceMappingURL=reader.js.map