UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

377 lines • 17.8 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 { GENERAL_COMMANDS } from "../../commands/builtin/general/index.js"; import { GENERIC_COMMANDS } from "../../commands/builtin/generic_bind/factory.js"; import { standardRequest } from "../../commands/spec/standard.js"; import { CommandSpec } from "../../commands/spec/types.js"; import { nodeHelp, ownsArgv } from "../../commands/cli/walk.js"; import { effectivePathMode } from "../../context/session_context.js"; import { decide } from "../../policy/match/decide.js"; import { patternReaches, splitPattern } from "../../policy/match/pattern.js"; import { Outcome } from "../../policy/types.js"; import { MountMode, PathSpec } from "../../types.js"; import { isGlob, pathVisible } from "../../utils/hidden.js"; import { compareCodePoints } from "../../utils/sort.js"; import { unlistedService } from "../node/admission.js"; import { ASKS, CHANGES, INTRO, READS, REFUSED, WALKS } from "./constants.js"; import { lineFacts } from "../lookup/index.js"; const SHARED = new Set([...GENERIC_COMMANDS, ...GENERAL_COMMANDS].map((c) => c.name)); const SEARCHES = ['grep -r', 'rg']; const FINDS = ['grep', 'rg']; /** * The mounts the documents describe, each with its prefix and the mode in * force there: every one the session sees but mirage's own. */ function* listed(registry, session) { for (const mount of [...registry.visibleMounts()].sort((a, b) => compareCodePoints(a.prefix, b.prefix))) { const prefix = mount.prefix.replace(/\/$/, '') || '/'; if (['/dev', '/usr/bin', '/.bash_history'].includes(prefix) || ['dev', 'history', 'bin', 'document'].includes(mount.vfs.name) || !pathVisible(session.visibility, prefix)) continue; yield [mount, prefix, effectivePathMode(prefix, mount.prefix, mount.mode, session)]; } } /** * Whether the session's profile lets a line of `words` run in `cwd`: the * documents name nothing the profile refuses. Empty `words` is a * redirection, which no rule names. `inside` names a file under `cwd`, so * that a rule written for paths there speaks; a CLI names none. `registry` * holds the CLIs, so that a CLI reaching a service is refused as execution * refuses it under a profile that hides paths; null for a file command. */ function allowed(session, cwd, words, inside = false, registry = null) { const [command, ...argv] = words; if (command === undefined) return true; const paths = inside ? [PathSpec.fromStrPath(`${cwd.replace(/\/+$/, '')}/<file>`)] : []; const facts = lineFacts(command, words, cwd); const ctx = { command, paths, argv, cwd, subtree: facts.subtree, moveTarget: facts.moveTarget, }; if (registry !== null && unlistedService(ctx, registry, session)) return false; return decide(ctx, session.commands).outcome !== Outcome.DENY; } /** * The lines that stand for `words` under the session's profile: the words as * they stand, and followed by what each longer pattern in its allow list adds * (`rm -f` where the list holds only that), unless `spec` answers that line * with `--help` or `--version`. A null `spec` counts every form. */ function forms(session, words, spec) { const found = [words]; const [command] = words; const allow = session.commands?.allow ?? null; if (command !== undefined && allow !== null) { for (const pattern of allow) { const form = [...words, ...splitPattern(pattern).slice(words.length)]; if (form.length > words.length && patternReaches(pattern, words) && standardRequest(command, spec, form.slice(1)) === null) found.push(form); } } return found; } /** * Whether the profile lets some line of `words` do its work on a file under * `path`. A form starting with one of the `claimed` longer commands, which * are answered on their own, does not count here. */ function runnable(session, mount, path, words, claimed = []) { const [command] = words; const spec = command === undefined ? null : mount.specFor(command); return forms(session, words, spec) .filter((form) => !claimed.some((k) => k.every((w, i) => form[i] === w))) .some((form) => allowed(session, path, form, true)); } /** * Whether the profile lets `change` run at `path`. A longer form another * change names counts for that change alone where it runs on other ops * (`tee -a` appends where `tee` writes); one running on more ops (`rm -r`) * does this change's work on a file too. */ function admitted(session, mount, path, change) { const size = change.command.length; const claimed = CHANGES.filter((other) => other.command.length > size && change.command.every((w, i) => other.command[i] === w) && !change.needs.every((ops) => other.needs.some((more) => ops.every((op) => more.includes(op))))).map((other) => other.command); return runnable(session, mount, path, change.command, claimed); } /** * Whether the profile lets the mount's own command `name` run at `path` * without changing files, and whether it lets it change them. A command tree * answers leaf by leaf, each by its own `write`. */ function uses(session, mount, path, name) { const tree = mount.cliFor(name); if (tree === null) { const ok = runnable(session, mount, path, [name]); const writes = mount.mayWrite(name); return [ok && !writes, ok && writes]; } const leaves = Object.entries(tree.handlers) .filter(([, handler]) => handler.fn !== null) .map(([verbs, handler]) => [ handler.write, runnable(session, mount, path, [name, ...verbs.split(' ').filter(Boolean)]), ]); return [leaves.some(([write, ok]) => ok && !write), leaves.some(([write, ok]) => ok && write)]; } /** The mount's own commands that the profile lets change files at `path`. */ function writers(session, mount, path) { return Object.keys(mount.commands()) .filter((n) => !SHARED.has(n)) .sort(compareCodePoints) .filter((n) => uses(session, mount, path, n)[1]); } function works(mount, change) { return change.needs.some((ops) => ops.every((op) => mount.answers(op))); } function code(text) { return '`' + text.replaceAll('|', '\\|') + '`'; } function listing(items) { if (items.length < 2) return items.join(''); return `${items.slice(0, -1).join(', ')} and ${items.at(-1) ?? ''}`; } function agree(items, verb) { return items.length > 1 ? verb : verb + 's'; } /** `text` in a code fence longer than any fence line inside it, spaces around one included. */ function fenced(text, info) { const fence = '`'.repeat(Math.max(3, ...text .split('\n') .map((s) => s.trim()) .filter((s) => /^`+$/.test(s)) .map((s) => s.length + 1))); return `${fence}${info}\n${text}\n${fence}`; } function table(head, rows) { return [ `| ${head[0]} | ${head[1]} |`, '| --- | --- |', ...rows.map(([path, text]) => `| ${path} | ${text} |`), ].join('\n'); } /** * The mode VFS.md states for `path`: `read` where a write mode lets no * command the profile allows change a file there, neither a Unix one nor * one of the mount's own. */ function granted(session, mount, path, mode) { if (mode === MountMode.WRITE && writers(session, mount, path).length === 0 && !CHANGES.some((c) => admitted(session, mount, path, c) && works(mount, c))) return MountMode.READ; return mode; } /** What one path supports, as VFS.md states it. */ function supported(session, mount, path, mode) { const runs = (words) => runnable(session, mount, path, words); const reads = READS.filter((c) => runs([c])).map(code); const changes = CHANGES.filter((c) => admitted(session, mount, path, c)); const working = changes .filter((c) => mode !== MountMode.READ && works(mount, c)) .map((c) => c.label); const unrestricted = reads.length === READS.length && changes.length === CHANGES.length; const parts = []; if (unrestricted && working.length > 0 && 2 * working.length > changes.length) { parts.push('Almost all Unix commands.'); } else { let text = 'Commands that read files'; if (reads.length > 0) text += ` (${reads.join(', ')} and the rest)`; if (working.length > 0) text += `, and ${listing(working)}`; parts.push(text + '.'); } const name = code(mount.vfs.name); const word = mount.vfs.wholeWordSearch ? ['-w'] : []; const searches = SEARCHES.filter((c) => runs([...c.split(' '), ...word])).map(code); if (searches.length > 0 && mount.answers('files_containing')) { const wordText = word.length > 0 ? ' with `-w`' : ''; parts.push(`${listing(searches)}${wordText} ${agree(searches, 'ask')} ${name} which files may match.`); } const finds = FINDS.filter((c) => runs([c])).map(code); if (finds.length > 0 && mount.answers('lines_containing')) parts.push(`${listing(finds)} ${agree(finds, 'ask')} ${name} for the matching lines.`); const asks = ASKS.filter(([op, command]) => mount.answers(op) && runs(command.split(' '))).map(([, command]) => code(command)); if (asks.length > 0) parts.push(`${listing(asks)} ${agree(asks, 'ask')} ${name} instead of reading a file.`); const offered = (name) => { const [reads, writes] = uses(session, mount, path, name); return reads || (writes && mode !== MountMode.READ); }; const own = Object.keys(mount.commands()) .filter((n) => !SHARED.has(n)) .sort(compareCodePoints) .filter(offered) .map(code); if (own.length > 0) parts.push(`Also ${listing(own)}.`); return parts.join(' '); } /** Where one path differs from a local filesystem. */ function limitations(session, mount, path, mode) { const runs = (words) => runnable(session, mount, path, words); const parts = []; if (mode === MountMode.READ) { parts.push('Read only: no command changes files.'); } else { const changes = CHANGES.filter((c) => admitted(session, mount, path, c)); const missing = changes.filter((c) => !works(mount, c)).map((c) => c.label); if (missing.length > 0 && missing.length === changes.length) parts.push('No Unix command changes files.'); else if (missing.length > 0) parts.push(`Not supported: ${missing.join(', ')}.`); } const vfs = mount.vfs; const word = vfs.wholeWordSearch ? ['-w'] : []; const searches = SEARCHES.filter((c) => runs([...c.split(' '), ...word])).map(code); if (searches.length > 0 && mount.answers('files_containing')) { const reads = `${listing(searches)} ${agree(searches, 'read')}`; if (vfs.wholeWordSearch) parts.push(`Without \`-w\`, ${reads} every file.`); if (vfs.searchable !== null) parts.push(`${code(vfs.name)} answers only for ${listing(vfs.searchable.map(code))}; ` + `${reads} every other file.`); } if (vfs.walkLimit >= 0) { const limit = String(vfs.walkLimit); const walks = WALKS.filter((c) => runs(c.split(' '))).map(code); const refused = REFUSED.filter((c) => runs(c.split(' '))).map(code); let text = walks.length > 0 ? `${listing(walks)} ${agree(walks, 'stop')} after ${limit} files; a glob matching more` : `A glob matching more than ${limit} files`; text += refused.length > 0 ? `, ${listing(refused)} are refused` : ' is refused'; parts.push(text + '.'); } return parts.join(' ') || 'None.'; } /** * Describe the current namespace without enumerating backend data: each * path, its layout, and the Unix commands it supports. */ export function vfsMd(registry, session) { const paths = []; const layouts = []; const commands = []; const limits = []; const vis = session.visibility; for (const [mount, prefix, mode] of listed(registry, session)) { const shown = granted(session, mount, prefix, mode); paths.push([code(prefix), `${code(mount.vfs.name)}, ${shown}`]); commands.push([code(prefix), supported(session, mount, prefix, shown)]); limits.push([code(prefix), limitations(session, mount, prefix, shown)]); // Backend prose is not a permission document. Under a restricted view // it can advertise hidden paths or verbs, so describe only structured // facts rather than interpolate unrestricted guidance. if (vis.paths === null && vis.shown === null && session.commands === null && mode === mount.mode && mount.vfs.vfsMd.trim() !== '') { const layout = mount.vfs.vfsMd .replaceAll('{prefix}', prefix) .replace(/^\n+|\n+$/g, '') .trimEnd(); // A backtick or line break in the prefix could close a code span or // fence it is put in, so such a layout stays text. layouts.push(`### ${code(prefix)}\n\n${/[`\n\r]/.test(prefix) ? fenced(layout, 'text') : layout}`); } for (const entry of vis.shown?.entries ?? []) { if (entry.mode === null || isGlob(entry.path) || !pathVisible(vis, entry.path)) continue; if (registry.tryMountFor(entry.path) === mount) { const effective = granted(session, mount, entry.path, effectivePathMode(entry.path, mount.prefix, mount.mode, session)); const at = code(entry.path); paths.push([at, `${code(mount.vfs.name)}, ${effective}`]); commands.push([at, supported(session, mount, entry.path, effective)]); limits.push([at, limitations(session, mount, entry.path, effective)]); } } } const parts = [ '---\nname: workspace-filesystem\ndescription: The virtual filesystem layout of this workspace.\n---', '# Virtual Filesystem', INTRO, ]; if (paths.length === 0) { parts.push('No paths are visible.'); } else { parts.push(table(['Path', 'Description'], paths), ...(layouts.length > 0 ? ['## VFS Layout', ...layouts] : []), '## Supported Unix Commands for VFS', 'Unix commands work on each path as on a local filesystem, as far as the path supports them:', table(['Path', 'Supported'], commands), '## Limitations of Unix Commands for VFS', 'Where a path differs from a local filesystem:', table(['Path', 'Limitations'], limits)); } parts.push("Run `man <cmd>` or `<cmd> --help` for one command's usage."); return parts.join('\n\n') + '\n'; } /** * The help pages of one CLI node and its descendants the profile allows. A * node gets a page when the profile allows it or leads to a descendant it * allows, so a group's own options stay documented; a page lists only the * children leading to an allowed one. A leaf counts a longer form its allow * list names (`gws gmail read --id`). */ export function cliPages(head, node, style, session, registry, path = []) { const children = node.subcommands.map((child) => [child, cliPages(head, child, style, session, registry, [...path, child.name])]); const pages = []; const words = [head, ...path]; const kept = children.filter(([, found]) => found.length > 0).map(([child]) => child); if (kept.length > 0 || (children.length === 0 ? forms(session, words, null) : [words]).some((form) => allowed(session, session.cwd, form, false, registry))) { const name = words.join(' '); const init = node; const text = nodeHelp(name, new CommandSpec({ ...init, subcommands: kept }), style).trimEnd(); pages.push(`## \`${name}\`\n\n${fenced(text, 'text')}`); } for (const [, found] of children) pages.push(...found); return pages; } /** * Render the skill: the registered CLIs the session's profile lets it run, * from their specs. Mirrors Python's `skill_md`. */ export function skillMd(registry, session) { const parts = [ '---\nname: workspace-commands\ndescription: The CLIs registered in this workspace and how to call them.\n---', '# Available commands', ]; const pages = []; for (const [head, install] of [...registry.clis.items()].sort(([a], [b]) => compareCodePoints(a, b))) { const found = cliPages(head, install.cli.spec, install.cli.spec.usageStyle, session, registry); for (const item of found) pages.push(item); if (found.length > 0 && ownsArgv(install.cli)) pages.push('This program parses its own arguments; only its registered description is available here.'); } if (pages.length > 0) { parts.push("These commands are registered with this shell; `<cmd> --help` shows each one's usage.", ...pages); } else { parts.push('No CLIs are registered.'); } return parts.join('\n\n') + '\n'; } //# sourceMappingURL=render.js.map