@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
274 lines • 12.2 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 { compileSpec } from "../../commands/spec/compile.js";
import { EXTERNAL_COMMANDS } from "../../runtime/constants.js";
import { LanguageRuntime } from "../../runtime/language.js";
import { isLineExecutor, isProcessExecutor } from "../../runtime/mixin.js";
import { CAPTURABLE_BUILTINS, EXEC_WRAPPERS, INTERPRETER_NAMES, KEYWORDS, NAMESPACE_COMMANDS, ROUTED_COMMANDS, SHELL_NAMES, SHELL_ONLY_BUILTINS, } from "./constants.js";
import { Consumer } from "./types.js";
import { shellQuote } from "../../utils/quote.js";
import { compareCodePoints } from "../../utils/sort.js";
/**
* Whether a command word is a tool the allow lists govern. Every named
* command is a subject, shell builtins included: an allow list stating
* `cat` leaves no `echo` and no `cd`. Two kinds of word are not,
* because neither is a name the list could hold: a path being executed
* (its lines are each checked as they run), and the agent's own
* function where the function is what runs, which in this shell means
* a name no builtin owns (builtins shadow functions), and its body's
* lines each pass the gate themselves.
*/
export function isTool(name, session) {
if (name.includes('/'))
return false;
return !(name in session.functions && !SHELL_NAMES.has(name));
}
/** Whether routing explicitly refused the external runtime for `name`. */
export function runtimeRefused(name, session, registry, routing) {
if (routing === undefined)
return false;
const key = Object.hasOwn(routing.bindings, name) ? name : EXTERNAL_COMMANDS;
return (Object.hasOwn(routing.bindings, key) &&
routing.bindings[key] === null &&
lookup(name, session, registry) === Consumer.EXTERNAL);
}
/**
* Whether the runtime runs the name as a native program. A language runtime
* that also runs processes (local) still interprets its own language's
* names (python3), so those stay the session's builtin; any other name it
* captures runs natively.
*/
function isNative(runtime, name) {
if (runtime == null || !(isLineExecutor(runtime) || isProcessExecutor(runtime)))
return false;
if (!(runtime instanceof LanguageRuntime))
return true;
return !runtime.constructor.commands.includes(name);
}
/**
* Yield every layer holding the name, most-preferred first.
*
* The one place precedence is written down: `lookup` reads the first
* yield and `lookupAll` reads all of them. Lazy on purpose, so the winner
* costs exactly what it did before the split (a name an installed CLI
* answers never reaches the mount lookup). A profile's allow list does
* not narrow it: a word the list leaves out is found, and the policy gate
* refuses it. `routing` is the line's placement; outside a line the static
* bindings answer.
*/
function* layers(name, session, registry, routing) {
let found = false;
const bindings = routing?.bindings ?? registry.runtimeBindings;
const bound = Object.hasOwn(bindings, name) ? bindings[name] : undefined;
const native = isNative(bound, name);
const refused = runtimeRefused(name, session, registry, routing);
if (SHELL_NAMES.has(name)) {
found = true;
yield (native || refused) && CAPTURABLE_BUILTINS.has(name)
? Consumer.EXTERNAL
: Consumer.SESSION;
}
if (NAMESPACE_COMMANDS.has(name)) {
found = true;
yield Consumer.NAMESPACE;
}
if (Object.hasOwn(session.functions, name)) {
found = true;
yield Consumer.FUNCTION;
}
if (registry.clis.get(name) !== null) {
found = true;
yield Consumer.CLI;
}
if ((native || refused) && !SHELL_NAMES.has(name)) {
found = true;
yield Consumer.EXTERNAL;
}
if (ROUTED_COMMANDS.has(name) || registry.mountForCommand(runsAs(name, registry)) !== null) {
found = true;
yield Consumer.MOUNT;
}
const fallback = Object.hasOwn(bindings, EXTERNAL_COMMANDS)
? bindings[EXTERNAL_COMMANDS]
: undefined;
const fallbackNative = fallback !== null &&
fallback !== undefined &&
(isLineExecutor(fallback) || isProcessExecutor(fallback));
if (!found && !Object.hasOwn(bindings, name) && (fallbackNative || refused)) {
yield Consumer.EXTERNAL;
}
}
/**
* Route a command name to the layer that consumes it.
*
* Order mirrors dispatch precedence: shell builtins shadow functions,
* functions shadow installed CLIs, CLIs shadow mount commands, and a
* name nobody registers is UNKNOWN (command not found). Install-time
* collision rules keep the CLI arm honest: a CLI may not take a shell
* builtin's or a general command's name, so the only shadowing a CLI
* can actually exert is over a mount-specific custom command.
*
* The full landscape, in precedence order. The column to watch is what
* resolves the name: session or workspace state for the named layers,
* operand paths for mounts:
*
* Consumer Example Resolved by Words
* SESSION cd, echo, export name in SHELL_NAMES shell-expanded
* NAMESPACE ln -s, readlink NAMESPACE_COMMANDS shell-expanded
* FUNCTION deploy() {..} session.functions shell-expanded
* CLI slack message send registry.clis shell-expanded
* MOUNT grep, cat, du operand paths pushdown
* UNKNOWN bogus nobody untouched, 127
*
* Named process captures select EXTERNAL before mount commands.
* EXTERNAL_COMMANDS handles names no preceding layer owns.
*
* This is the winner only. A name can sit in more than one layer at once
* (a function shadowing an installed CLI); `lookupAll` reports them all,
* which is what `type -a` prints. Reading one item off the generator is
* what makes that sharing free: the lookups after the winner never run,
* so dispatch pays exactly what it did when this was a chain of `if`
* arms.
*/
export function lookup(name, session, registry, routing) {
for (const consumer of layers(name, session, registry, routing))
return consumer;
return Consumer.UNKNOWN;
}
/**
* Every layer holding the name, most-preferred first.
*
* Empty when nothing holds it, where `lookup` says UNKNOWN. Only
* introspection (`type -a`, `which -a`) needs this: dispatch runs the
* winner and never asks what it shadowed.
*/
export function lookupAll(name, session, registry) {
return [...layers(name, session, registry)];
}
/**
* The layer a name runs from as a program, null when it is none.
*
* A program is what a real system ships as a file on PATH, so it has one
* under `/usr/bin` here: every mount, namespace and CLI command, every
* name a runtime captures by name, and each builtin a real system also
* finds on disk (echo, test, xargs). The shell's own words (cd, export,
* history: `SHELL_ONLY_BUILTINS`), reserved words, functions and aliases
* have no file, and a shell word keeps none when a mount registers the
* same name, since the builtin is what runs. Nor does a name only the
* external fallback capture takes: it takes any word, so like bash's
* `command_not_found_handle` it runs a name without making it a program.
* An interpreter (python3, node) is a program only where a language
* runtime is bound to it: without one it answers 127, as it does on a
* system that never installed it. A function shadowing a program leaves
* the file in place, as it does on PATH.
*/
export function program(name, session, registry) {
if (name.includes('/') || KEYWORDS.has(name))
return null;
for (const consumer of layers(name, session, registry)) {
if (consumer === Consumer.FUNCTION)
continue;
if (consumer === Consumer.SESSION && SHELL_ONLY_BUILTINS.has(name))
return null;
if (consumer === Consumer.SESSION &&
INTERPRETER_NAMES.has(name) &&
!(registry.runtimeBindings[name] instanceof LanguageRuntime))
return null;
if (consumer === Consumer.EXTERNAL && registry.runtimeBindings[name] === undefined)
return null;
return consumer;
}
return null;
}
/**
* Whether a builtin that execs its operand (xargs, timeout) finds it.
*
* GNU execs the name, so a shell word with no file on PATH (cd, export) is
* missing there, as is a name nothing provides. A function runs too, where it
* is what the name runs (a shell word shadows one): mirage runs the line in
* the shell, which is how these reach it. A path is left to the run, which
* reports on it.
*/
export function execs(name, session, registry) {
if (name.includes('/') || program(name, session, registry) !== null)
return true;
const consumer = lookup(name, session, registry);
return consumer === Consumer.FUNCTION || consumer === Consumer.EXTERNAL;
}
/**
* What a program's `/usr/bin` file says about it, null when the name is no
* program.
*
* One line: what runs the name, and `--help` where the program's spec
* answers it, which a mount command's and a CLI's do; a builtin's answer
* varies (`ln --help` and `echo --help` print no help), so a builtin's
* line names none.
*/
export function programNote(name, session, registry) {
const consumer = program(name, session, registry);
if (consumer === null)
return null;
const runtime = registry.runtimeBindings[name];
if (runtime !== undefined && (consumer === Consumer.EXTERNAL || INTERPRETER_NAMES.has(name))) {
return `${name} runs on the workspace's ${runtime.name} runtime.`;
}
const helpLine = ` Help: ${shellQuote(name)} --help`;
if (consumer === Consumer.CLI) {
return `${name} is a CLI registered with this workspace.${helpLine}`;
}
const runs = runsAs(name, registry);
const spec = consumer === Consumer.MOUNT ? (registry.mountForCommand(runs)?.specFor(runs) ?? null) : null;
if (spec !== null &&
compileSpec(spec).options.some((option) => option.names.includes('--help'))) {
return `${name} is built into mirage.${helpLine}`;
}
return `${name} is built into mirage.`;
}
/**
* The program a command word runs: a wrapper no mount registers (`egrep`)
* runs the program it names, any other word itself.
*/
export function runsAs(name, registry) {
const wrapped = EXEC_WRAPPERS.get(name);
return wrapped !== undefined && registry.mountForCommand(name) === null ? wrapped[0] : name;
}
/**
* Every program name the session can run, sorted: the `/usr/bin` listing.
*
* The names are gathered from each layer that can hold a program and
* kept only where `program` says the name runs as one, so the listing and
* a lookup never disagree. A name only the external fallback capture
* would take cannot be listed, since that capture takes any word.
*/
export function programs(session, registry) {
const names = new Set([
...SHELL_NAMES,
...NAMESPACE_COMMANDS,
...ROUTED_COMMANDS,
...EXEC_WRAPPERS.keys(),
...registry.clis.names(),
]);
for (const capture of Object.keys(registry.runtimeBindings)) {
if (capture !== EXTERNAL_COMMANDS)
names.add(capture);
}
for (const mount of registry.allMounts()) {
for (const cmd of mount.allCommands())
names.add(cmd.name.split(' ')[0] ?? cmd.name);
}
return [...names]
.filter((name) => program(name, session, registry) !== null)
.sort(compareCodePoints);
}
//# sourceMappingURL=lookup.js.map