UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

125 lines • 6.04 kB
// ========= 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 { fromUnits, units } from "../rs/document.js"; import { admissionMatcher, linkTraceWire, visibilityMatcher } from "../rs/policy.js"; import { traceSymlinks } from "./path.js"; /** Whether a document entry is a pattern (any of `*`, `?`, `[`). */ export function isGlob(entry) { return admissionMatcher(null).ask('is_glob', { entry: units(entry) }) === true; } /** * Compile document path entries into the matcher's shape: glob = * pattern, plain = exact subtree, in the order written. The same split * serves `paths.hide` and a `CommandRule`'s `paths`, so both planes * match the same way. Null when there is nothing to match, * which is what "unrestricted" reads as. */ export function classifyPaths(entries) { const scope = admissionMatcher(null).ask('classify_paths', { entries: entries.map(units) }); if (scope.paths.length + scope.patterns.length === 0) return null; return { paths: scope.paths.map(fromUnits), patterns: scope.patterns.map(fromUnits) }; } /** * The place a show entry anchors to: the entry itself when exact, the * fixed directory above its first glob segment when a pattern. */ export function showHead(entry) { return fromUnits(admissionMatcher(null).ask('show_head', { entry: units(entry) })); } /** * Whether one session's path axis leaves this virtual path visible: * the whole composition law for the VFS axis. * * Three steps, each the anchor-depth rule: no hide covers the path and * it is visible; a show covers it more deeply than the deepest hide * and it is visible (hide wins the tie); and a hidden directory stays * visible when a visible show anchors strictly below it, so the road * to a carve-out exists (`hide /repo` + `show /repo/public` keeps * `/repo` listable, holding only the carve-out). */ export function pathVisible(vis, virtual) { return (vis?.paths == null || visibilityMatcher(vis).ask('path_visible', { path: units(virtual) }) === true); } /** * Whether relocating `src` to `dst` could surface a hidden path. * * The reveal half of the subtree law: a session's mutation may destroy * what it cannot see (`rm -r`, a remnant `rmdir`), but may never reveal * it, and a rename or a native directory copy is the relocation that * would. A source the session reaches only through a show below it is * hidden itself, so whatever the show does not reopen reveals wherever the * session sees `dst`. Beyond that, three arms, one per entry kind. A * component pattern follows * the name wherever the content goes, so it never reveals. An exact * entry anchored strictly below `src` re-anchors to `dst` plus its * suffix, and reveals when the session would see that mapped path (a * show anchored below the mapped path counts, through `pathVisible`'s * own carve-out rule). An anchored pattern's coverage does not move * with the content, so any pattern whose match space could reach below * `src` refuses, failing toward refusal the way `readonlyBelow` blames * a pattern. */ export function moveReveals(vis, src, dst) { return (vis?.paths != null && visibilityMatcher(vis).ask('move_reveals', { src: units(src), dst: units(dst) }) === true); } /** * Whether a visible name leads where the session cannot see: a link the walk * follows names a hidden path. The walk judges what a link brings in, not the * typed path, which the caller judges whole (a carve-out keeps its hidden * parents traversable): each link's own path, every name a stored target * spells before a later `..` takes it back (`/data/secret/../pub` still * prints `secret`), and the place the walk lands. Such a link is as hidden as * its target, since its listing row, `readlink` and `ls -l` would print the * hidden name; a link that leads nowhere (dangling, or a loop) leads nowhere * hidden. Mirrors Python's leads_hidden. */ export function leadsHidden(vis, virtual, links) { return (vis?.paths != null && links.size > 0 && visibilityMatcher(vis).ask('link_trace_hidden', { trace: linkTraceWire(traceSymlinks(virtual, links)), }) === true); } export function visibleLinks(vis, paths, links) { if (vis?.paths == null) return paths.map(() => true); const matcher = visibilityMatcher(vis); const shown = matcher.ask('visible_links', { candidates: paths.map((path) => ({ path: units(path), trace: [] })), }); const checked = matcher.ask('visible_links', { candidates: paths .filter((_, index) => shown[index]) .map((path) => ({ path: units(path), trace: linkTraceWire(traceSymlinks(path, links)) })), }); let index = 0; return shown.map((keep) => keep && checked[index++] === true); } /** * Whether the session could hide anything at or under this path, for * the view a native walk asks before it trusts the raw tree: a hide at * or under the path, or a pattern whose head could reach it. */ export function hiddenUnder(vis, virtual) { return (vis?.paths != null && visibilityMatcher(vis).ask('hides_intersect', { path: units(virtual) }) === true); } /** Whether the session hides this variable name. */ export function varHidden(vis, name) { return (vis?.vars != null && visibilityMatcher(vis).ask('var_hidden', { name: units(name) }) === true); } //# sourceMappingURL=hidden.js.map