@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
224 lines • 9.83 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 { 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