UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

224 lines • 9.83 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 { compareCodePoints } from "../utils/sort.js"; import { EXTERNAL_COMMANDS, WHOLE_LINE } from "./constants.js"; import { QuickJsRuntime } from "./js/quickjs/runtime.js"; import { isLineExecutor, isProcessExecutor } from "./mixin.js"; import { MontyRuntime } from "./python/monty/index.js"; import { PyodideRuntime } from "./python/pyodide/runtime.js"; import { WorkspaceRuntime } from "./workspace.js"; // Null prototype: runtime names come from config, and a name like // `constructor` must miss instead of resolving an `Object.prototype` // member as a runtime class. const NAMED = Object.assign(Object.create(null), { pyodide: PyodideRuntime, monty: MontyRuntime, quickjs: QuickJsRuntime, workspace: WorkspaceRuntime, }); /** * The default world when no runtimes list is given: one python engine, * one js engine, and the builtin command engine, the same three as * Python's (pinned by integ/runtime/routing/defaults.json). Names, not * classes: `name` is an instance field, so `MontyRuntime.name` is the JS * function name, not the registry key. */ export const DEFAULT_ENTRIES = ['monty', 'quickjs', 'workspace']; // The runtimes @struktoai/mirage-node registers when it is imported. const NODE_RUNTIMES = [ 'apple_container', 'daytona', 'docker', 'local', 'microsandbox', 'sandlock', 'smolvm', 'ssh', ]; const NODE_HINT = 'lives in @struktoai/mirage-node; import that package to register it. Browser ' + "worlds support 'monty' (sandboxed, default), 'pyodide' (WASM CPython), and " + "'quickjs' (sandboxed JavaScript)"; /** Why a name a config may carry is not registered here, and where it lives. */ const RUNTIME_HINTS = { ...Object.fromEntries(NODE_RUNTIMES.map((name) => [name, `runtime '${name}' ${NODE_HINT}`])), wasi: "runtime 'wasi' is Python-only (a CPython WASI build); TypeScript " + "supports 'monty' (sandboxed, default), 'pyodide' (WASM CPython), " + "and 'quickjs' (sandboxed JavaScript)", sandlock: "runtime 'sandlock' lives in @struktoai/mirage-node; import that package " + 'to register it. Sandlock requires Linux and `sandlock` on PATH.', e2b: "runtime 'e2b' registers when its module is imported: " + "'@struktoai/mirage-core/runtime/sandbox/e2b/runtime' in a browser, or " + '@struktoai/mirage-node, which imports it', }; // Every runtime is constructed the same way; config keys are checked // inside each class (its config key list), so the entry level only // knows the uniform options. Python gets this check for free // (`**options` raises TypeError on an unknown kwarg); a TS object // literal would silently swallow a typo key without it. const ENTRY_KEYS = ['captures', 'config']; // The names mirage ships: core's own, and the ones its packages register // when they load, so `registerRuntime` can refuse to shadow one the way // the VFS and CLI registries refuse a builtin name. A package's own // registration is the first under its name; any later one shadows it. const BUILTIN_RUNTIMES = new Set([ ...Object.keys(NAMED), ...NODE_RUNTIMES, 'e2b', ]); /** * Register a runtime class under a config name. Host-side only, like * `registerVfs` and `registerCliSpec`: the embedding program * calls it, never a line the agent types. Once registered the name works * everywhere a builtin's does: a `runtimes:` entry in workspace config, a * string in `new Workspace(..., { runtimes })`, and `execute({ runtime })`. * Runtime packages use the same entry point for their own runtimes (`daytona` * from `@struktoai/mirage-node`). Mirrors `register_runtime` in * `mirage/runtime/table.py`: a core builtin cannot be shadowed, and * re-registering any other name replaces it. */ export function registerRuntime(name, cls) { if (BUILTIN_RUNTIMES.has(name) && NAMED[name] !== undefined) { throw new Error(`cannot register '${name}': shadows a builtin`); } NAMED[name] = cls; } /** Every name `buildRuntime` can resolve, builtin and registered. */ export function knownRuntimes() { return Object.keys(NAMED).sort(compareCodePoints); } /** * Refuse an entry key no runtime takes, naming the entry. Every runtime * is constructed the same way, so this is one check for a builtin, a * registered name and a `source:Class` reference alike: `buildRuntime` * runs it for the names it resolves, and the config loader runs it for * a reference, whose class it constructs itself. */ export function checkRuntimeOptions(name, options) { for (const key of Object.keys(options)) { if (!ENTRY_KEYS.includes(key)) { const knownKeys = ENTRY_KEYS.map((k) => `'${k}'`).join(', '); throw new Error(`unknown ${name} runtime option '${key}' (expected: ${knownKeys})`); } } } /** * Construct a runtime by name, failing loud on unknown names (with a * cross-language hint for Python-only names) and on unknown options. */ export function buildRuntime(name, options = {}) { const cls = NAMED[name]; if (cls === undefined) { const hint = RUNTIME_HINTS[name]; if (hint !== undefined) throw new Error(hint); const known = knownRuntimes() .map((n) => `'${n}'`) .join(', '); throw new Error(`unknown runtime: '${name}' (expected one of ${known})`); } checkRuntimeOptions(name, options); return new cls(options); } /** * Resolve an explicit runtime name into a binding override map. * * Naming a runtime places a line's captured stages on it without * touching capability: only commands the runtime captures rebind, * everything else keeps its normal binding. */ export function runtimeBindingsFor(entries, name) { if (name === 'workspace') { throw new Error(`'workspace' is the default executor, not a runtime you can select`); } for (const entry of entries) { if (entry.name === name) { // Null prototype: captures are arbitrary command names. const bindings = Object.create(null); for (const command of entry.captures) bindings[command] = entry; return bindings; } } const known = entries.map((e) => `'${e.name}'`).join(', '); throw new Error(`unknown runtime: '${name}' (workspace runtimes: ${known})`); } /** * Resolve the ordered world into a command -> runtime binding map. * * A command binds to the FIRST entry that captures it; a default workspace * runtime captures nothing, so only a workspace entry with declared captures * appears in the map. Duplicate names are rejected: a second entry * under the same name could never bind anything and always signals a * config mistake. */ export function bindCommands(entries) { // Null prototype: captures are arbitrary command names, so a capture // like `toString` must bind instead of reading as already-present. const bindings = Object.create(null); const seen = new Set(); for (const entry of entries) { for (const capture of entry.captures) { if (capture !== EXTERNAL_COMMANDS && capture !== WHOLE_LINE && (capture === '*' || capture.startsWith('@'))) { throw new Error(`'${capture}' is not a program or a group; the groups are '${EXTERNAL_COMMANDS}' and '${WHOLE_LINE}'`); } } if (entry.captures.includes(WHOLE_LINE) && !isLineExecutor(entry)) throw new Error(`${WHOLE_LINE} requires shell execution`); if (entry.captures.includes(EXTERNAL_COMMANDS) && !isLineExecutor(entry) && !isProcessExecutor(entry)) { throw new Error('@external requires process or shell execution'); } if (seen.has(entry.name)) { throw new Error(`duplicate runtime entry: '${entry.name}'`); } seen.add(entry.name); for (const command of entry.captures) { if (!(command in bindings)) bindings[command] = entry; } } return bindings; } /** * The runtime that runs this entire line, if any. * * Only an explicit WHOLE_LINE capture claims a whole line. Named captures * and EXTERNAL_COMMANDS execute individual commands. The workspace runtime never matches here * because it carries no capability: the workspace executor IS the * path a workspace-resolved line takes anyway, so there is no delegate. */ export function wholeLineRuntime(bindings) { const whole = Object.hasOwn(bindings, WHOLE_LINE) ? bindings[WHOLE_LINE] : null; if (whole != null && isLineExecutor(whole)) return whole; return null; } /** * The runtime that serves commands no entry captures, if any. * * That is the world's WorkspaceRuntime, unless it declares captures (then it * is an ordinary capturer and nothing is catch-all) or it is not among * the given entries. */ export function catchAll(entries) { for (const entry of entries) { if (entry instanceof WorkspaceRuntime && !entry.restricted) return entry; } return null; } //# sourceMappingURL=table.js.map