@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
496 lines • 22.1 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 { networkLayer, networkRoutes, NetworkRule } from "../io/network.js";
import { parseProcessPermissions } from "../process/config.js";
import { validateDocument } from "../rs/document.js";
import { ScriptSource } from "../runtime/types.js";
import { EnvVar, SecretRef } from "../secrets/config.js";
import { parseMountMode, weakerMode } from "../types.js";
import { normalizeFields, snakeToCamel } from "../utils/normalize.js";
import { norm } from "../utils/path.js";
import { isPlainObject } from "../utils/records.js";
import { compareCodePoints } from "../utils/sort.js";
import { checkCommandLimits, commandLimitsToJSON, parseCommandLimits, } from "./builtin/output_cap.js";
import { CTX_ENV, DEFAULT_ASK_REASON, DEFAULT_DENY_REASON, METHODS, PROGRAM_RUNTIME, } from "./constants.js";
const VARS_FIELDS = ['hide'];
const COMMANDS_FIELDS = ['allow', 'ask', 'deny'];
const MOUNT_COMMANDS_FIELDS = ['ask', 'deny'];
const PROFILE_MOUNT_FIELDS = ['mode', 'commands', 'paths'];
const PROFILE_FIELDS = [
'cwd',
'env',
'mounts',
'paths',
'vars',
'commands',
'policy',
'command_limits',
'processes',
'network',
];
const PROGRAM_FIELDS = ['script', 'runtime', 'env'];
function rejectUnknownKeys(block, allowed, where) {
for (const key of Object.keys(block)) {
if (!allowed.includes(key)) {
throw new Error(`${where}: unknown field \`${key}\` (allowed: ${allowed.join(', ')})`);
}
}
}
function asObject(raw, where) {
if (!isPlainObject(raw))
throw new Error(`${where} must be a mapping`);
return raw;
}
// An entry that names something must hold a token: a blank command
// pattern is a prefix of every line, so a stray "" would allow, ask about
// or deny every command, and a blank path entry is the root, so it would
// hide or deny the whole tree. `names` says what an entry names ("a
// command", "a path"); omitted, blank entries pass.
function stringList(raw, where, names) {
return validateDocument('strings', raw, where, names === undefined ? {} : { names });
}
/**
* Refuse a relative path entry. Every path in the document is absolute:
* an entry is either an absolute path or a name pattern (`*.pem`, no
* slash, matching a path component anywhere). A plain `xxx` or an
* anchored `secrets/*` would otherwise be read from the root (`/xxx`,
* `/secrets/*`), which is never what a relative spelling meant. There is
* no relative spelling anywhere: a mount section spells its paths in
* full and they are checked against the mount root (`underMount`),
* which is what a rebase used to do silently and wrongly (`/repo/secret`
* under `/repo` became `/repo/repo/secret`).
*/
function absolutePaths(entries, where) {
validateDocument('absolute', entries, where);
}
/**
* Refuse a path entry in a mount's section that leaves the mount. A
* mount's rules are about that mount, so a path under `mounts./repo`
* names something inside `/repo`. A name pattern carries no anchor and
* is left alone; it means the same thing here as anywhere else.
*
* The root mount contains everything, and has to be spelled out:
* `root + '/'` is `'//'` there, which no path starts with, so a
* workspace mounted at `/` could write a section for its one mount and
* then name nothing inside it.
*/
function underMount(entries, root, where) {
validateDocument('under', entries, where, { root });
}
/**
* Coerce a `deny` or `ask` list to its rules. A bare string is one
* command pattern over the whole line, with the arm's default reason. A
* mapping carries `reason` (defaulting) and exactly one of: `commands`
* as a list, a whole-line rule on each pattern; `commands` as a mapping,
* each command pattern on its own paths (one command to many paths, one
* rule per command); `paths` alone, a path rule on every command. A list
* of commands beside a list of paths is refused, because it does not say
* which command the paths belong to, and a rule naming neither is
* refused rather than read as "every command".
*/
function rules(raw, where, arm) {
const reason = arm === 'ask' ? DEFAULT_ASK_REASON : DEFAULT_DENY_REASON;
return validateDocument('rules', raw, `${where}.${arm}`, { reason });
}
function parseAllow(raw, where) {
if (raw === undefined || raw === null)
return null;
return stringList(raw, `${where}.allow`, 'a command');
}
/** Validate a `paths:` block. Every entry is absolute or a name pattern. */
export function PathsBlock(raw, where = 'paths') {
return validateDocument('paths', raw, where);
}
export function VarsBlock(raw, where = 'vars') {
const obj = asObject(raw, where);
rejectUnknownKeys(obj, VARS_FIELDS, where);
return { hide: stringList(obj.hide, `${where}.hide`) };
}
export function CommandsBlock(raw, where = 'commands') {
const obj = asObject(raw, where);
rejectUnknownKeys(obj, COMMANDS_FIELDS, where);
const ask = rules(obj.ask, where, 'ask');
const deny = rules(obj.deny, where, 'deny');
// This block is the profile's own, never a mount section's, so a rule's
// paths are virtual paths: absolute, or name patterns.
for (const rule of ask)
absolutePaths(rule.paths ?? [], `${where}.ask rule paths`);
for (const rule of deny)
absolutePaths(rule.paths ?? [], `${where}.deny rule paths`);
return { allow: parseAllow(obj.allow, where), ask, deny };
}
/** Validate a mount section's `commands:` block (`ask` and `deny` only). */
export function MountCommandsBlock(raw, where = 'commands') {
const obj = asObject(raw, where);
rejectUnknownKeys(obj, MOUNT_COMMANDS_FIELDS, where);
return { ask: rules(obj.ask, where, 'ask'), deny: rules(obj.deny, where, 'deny') };
}
/** Validate one `mounts.<prefix>` section of a profile. */
export function ProfileMount(raw, root, where) {
// A bare mode string is sugar for the section that carries only a mode.
const obj = typeof raw === 'string' ? { mode: raw } : asObject(raw, where);
rejectUnknownKeys(obj, PROFILE_MOUNT_FIELDS, where);
const out = {};
if (obj.mode !== undefined && obj.mode !== null) {
if (typeof obj.mode !== 'string')
throw new Error(`${where}.mode must be a mode name or alias`);
out.mode = parseMountMode(obj.mode);
}
if (obj.paths !== undefined && obj.paths !== null) {
const paths = PathsBlock(obj.paths, `${where}.paths`);
underMount(paths.hide, root, `${where}.paths.hide`);
underMount((paths.show ?? []).map((e) => e.path), root, `${where}.paths.show`);
out.paths = paths;
}
if (obj.commands !== undefined && obj.commands !== null) {
const commands = MountCommandsBlock(obj.commands, `${where}.commands`);
for (const rule of commands.ask ?? [])
underMount(rule.paths ?? [], root, `${where}.commands.ask`);
for (const rule of commands.deny ?? [])
underMount(rule.paths ?? [], root, `${where}.commands.deny`);
out.commands = commands;
}
return out;
}
/**
* Normalize a profile's `mounts` mapping: prefix to its settings, with a
* bare mode string as sugar for a section carrying only a mode. A bare
* list used to mean "only these mounts" and now means nothing at all,
* so it fails loudly rather than quietly dropping the confinement it
* used to carry.
*/
export function parseProfileMounts(raw, where = 'mounts') {
if (raw === undefined || raw === null)
return null;
let entries;
if (raw instanceof Map)
entries = [...raw.entries()];
else if (isPlainObject(raw))
entries = Object.entries(raw);
else
throw new Error(`${where} must be a mapping of prefix to its settings`);
const sections = new Map();
for (const [prefix, entry] of entries) {
if (typeof prefix !== 'string')
throw new Error(`${where} keys must be strings`);
const root = norm(prefix);
sections.set(root, ProfileMount(entry, root, `${where}[${root}]`));
}
return sections;
}
/**
* Validate a profile's `policy` block: per method, a list of programs,
* each naming its program and the runtime that runs it, monty when it
* names none.
* A config file spells the methods and a runtime's settings as the
* document does (`pre_command`, `auto_load_from_imports`); code, with
* `code` set, as the interface does (`preCommand`, `autoLoadFromImports`).
*/
export function parseProfilePolicy(raw, where, code = false) {
const keys = code ? Object.keys(METHODS) : Object.values(METHODS);
if (typeof raw === 'string' || raw instanceof ScriptSource) {
throw new Error(`${where} must be a mapping of method to programs`);
}
const obj = asObject(raw, where);
if ('script' in obj || 'runtime' in obj) {
throw new Error(`${where} names its programs per method, policy: ` +
`{pre_command: [{script: <file>, runtime: <runtime>}]}`);
}
const unknown = Object.keys(obj).filter((key) => !keys.includes(key));
if (unknown.length > 0) {
throw new Error(`${where} names ${unknown.sort(compareCodePoints).join(', ')}; a program answers at ` +
keys.join(', '));
}
const out = {};
for (const key of keys) {
const programs = obj[key];
if (programs === undefined)
continue;
if (!Array.isArray(programs))
throw new Error(`${where}.${key} must be a list of programs`);
out[snakeToCamel(key)] = programs.map((raw, i) => parseProgram(raw, `${where}.${key}[${String(i)}]`, code));
}
return out;
}
/**
* Validate one policy program, `{script, runtime}`: a profile's, or the
* workspace's `route_policy`. `code` reads a runtime's settings as the
* interface spells them, as `parseProfilePolicy` does.
*/
export function parseProgram(raw, at, code = false) {
const program = asObject(raw, at);
rejectUnknownKeys(program, PROGRAM_FIELDS, at);
const given = program.script;
if (given === undefined || given === null) {
throw new Error(`${at}.script names the policy's program, and is required`);
}
// A path, a ScriptSource code built, or the {source, language, module} a
// JSON profile spells one as, as Python's model reads it.
const fields = typeof given === 'object' && !(given instanceof ScriptSource) && !Array.isArray(given)
? given
: null;
if (!(typeof given === 'string' || given instanceof ScriptSource) &&
(fields === null ||
Object.keys(fields).some((key) => !['source', 'language', 'module'].includes(key)) ||
typeof fields.source !== 'string' ||
(fields.language !== undefined && fields.language !== 'python' && fields.language !== 'js') ||
(fields.module !== undefined && typeof fields.module !== 'boolean'))) {
throw new Error(`${at}.script must be a script path or source`);
}
const script = fields === null
? given
: new ScriptSource(fields.source, (fields.language ?? 'python'), fields.module === true);
let env = {};
if (program.env !== undefined) {
const entries = asObject(program.env, `${at}.env`);
if (CTX_ENV in entries) {
throw new Error(`${at}.env: a policy program's env cannot set ${CTX_ENV}, which carries its context`);
}
const parsed = Object.entries(entries).map(([name, value]) => {
const pointer = typeof value === 'string' ? null : SecretRef.safeParse(value);
if (pointer !== null && !pointer.success) {
throw new Error(`${at}.env.${name} must be a string or a {from, ref, key} pointer`);
}
return [name, pointer === null ? value : pointer.data];
});
env = { env: Object.fromEntries(parsed) };
}
const runtime = program.runtime === undefined ? PROGRAM_RUNTIME : program.runtime;
if (typeof runtime === 'string')
return { script, runtime, ...env };
const named = asObject(runtime, `${at}.runtime`);
const settings = named.config;
if (Object.keys(named).some((field) => field !== 'name' && field !== 'config') ||
typeof named.name !== 'string' ||
(settings !== undefined &&
(typeof settings !== 'object' || settings === null || Array.isArray(settings)))) {
throw new Error(`${at}.runtime: a policy runtime is a name or {name, config}, got ` + JSON.stringify(runtime));
}
if (settings === undefined)
return { script, runtime: { name: named.name }, ...env };
const config = settings;
return {
script,
runtime: { name: named.name, config: code ? config : normalizeFields(config) },
...env,
};
}
/**
* A policy program as it runs: its source, its runtime's name and that
* runtime's settings. `profile` is the profile it speaks for, empty for
* the workspace's `routePolicy`. Mirrors the Python `compile_program`.
*
* @throws Error - the script is still a path, which only the config
* loader loads.
*/
export function compileProgram(program, profile) {
if (typeof program.script === 'string') {
throw new Error(`names a policy by path ('${program.script}'); only the config loader loads one, ` +
`pass ScriptSource in code`);
}
const named = program.runtime ?? PROGRAM_RUNTIME;
const runtime = typeof named === 'string' ? { name: named, config: {} } : named;
return {
profile,
script: program.script,
runtime: runtime.name,
config: { ...runtime.config },
env: { ...program.env },
};
}
/** Validate one profile (a `profiles.<name>` block, or an inline document). */
export function SessionProfile(raw, where = 'profile') {
const obj = asObject(raw, where);
// The keys this was first shipped under, a `script` named for what
// the file is rather than what it does and a `runtime` beside it
// that read as the profile's own; refused with the new spelling
// rather than as two more unknown keys.
if (obj.script !== undefined || obj.runtime !== undefined) {
throw new Error(`${where}: script and runtime are now a policy block, ` +
`policy: {pre_command: [{script: <file>, runtime: <runtime>}]}`);
}
rejectUnknownKeys(obj, PROFILE_FIELDS, where);
const out = {};
if (obj.network != null) {
const layer = networkLayer(obj.network);
new NetworkRule(layer === null ? [] : [layer], false, networkRoutes(obj.network));
out.network = obj.network;
}
if (obj.command_limits != null) {
out.commandLimits = parseCommandLimits(obj.command_limits);
checkCommandLimits(out.commandLimits, 'profile', true);
}
if (obj.processes != null)
out.processes = parseProcessPermissions(obj.processes);
if (obj.policy !== undefined && obj.policy !== null) {
out.policy = parseProfilePolicy(obj.policy, `${where}.policy`);
}
if (obj.cwd !== undefined && obj.cwd !== null) {
if (typeof obj.cwd !== 'string')
throw new Error(`${where}.cwd must be a string`);
out.cwd = obj.cwd;
}
if (obj.env !== undefined && obj.env !== null) {
const env = asObject(obj.env, `${where}.env`);
for (const [k, v] of Object.entries(env)) {
if (typeof v === 'string')
continue;
if (!isPlainObject(v))
throw new Error(`${where}.env.${k} must be a string or an env entry`);
const parsed = EnvVar.safeParse(v);
if (!parsed.success) {
throw new Error(`${where}.env.${k}: ${parsed.error.issues.map((i) => i.message).join('; ')}`);
}
}
out.env = env;
}
if (obj.mounts !== undefined && obj.mounts !== null) {
out.mounts = parseProfileMounts(obj.mounts, `${where}.mounts`);
}
if (obj.paths !== undefined && obj.paths !== null)
out.paths = PathsBlock(obj.paths, `${where}.paths`);
if (obj.vars !== undefined && obj.vars !== null)
out.vars = VarsBlock(obj.vars, `${where}.vars`);
if (obj.commands !== undefined && obj.commands !== null)
out.commands = CommandsBlock(obj.commands, `${where}.commands`);
return out;
}
/**
* A profile as the document that parses back to it: the shape a
* `profiles:` entry has in YAML, which either host reads. Every block is
* spelled as the document spells it, a program's runtime settings
* included, and a script as its source, language and module flag.
* Mirrors Python `profile_to_dict`.
*/
export function profileToDict(profile) {
const doc = {};
if (profile.cwd != null)
doc.cwd = profile.cwd;
if (profile.env != null)
doc.env = { ...profile.env };
if (profile.vars != null)
doc.vars = { hide: [...profile.vars.hide] };
if (profile.policy != null) {
doc.policy = Object.fromEntries(Object.entries(profile.policy).map(([method, programs]) => [
METHODS[method],
programs.map(({ script, runtime = PROGRAM_RUNTIME, env }) => ({
script: typeof script === 'string'
? script
: { source: script.source, language: script.language, module: script.module },
runtime: typeof runtime === 'string'
? runtime
: {
name: runtime.name,
...(runtime.config !== undefined
? {
config: Object.fromEntries(Object.entries(runtime.config).map(([key, value]) => [
key.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`),
value,
])),
}
: {}),
},
...(env !== undefined && Object.keys(env).length > 0 ? { env: { ...env } } : {}),
})),
]));
}
if (profile.commandLimits != null)
doc.command_limits = commandLimitsToJSON(profile.commandLimits);
if (profile.processes != null)
doc.processes = parseProcessPermissions(profile.processes);
if (profile.network != null)
doc.network = profile.network;
if (profile.mounts != null) {
doc.mounts = Object.fromEntries([...profile.mounts].map(([prefix, entry]) => [prefix, mountDoc(entry)]));
}
if (profile.paths != null)
doc.paths = pathsDoc(profile.paths);
if (profile.commands != null)
doc.commands = commandsDoc(profile.commands);
return doc;
}
/**
* The profile a document states, as `profileToDict` writes it: a script
* written as its source comes back as the ScriptSource code passes.
* Mirrors Python `profile_from_dict`.
*/
export function profileFromDict(doc) {
if (!isPlainObject(doc.policy))
return SessionProfile(doc);
const policy = Object.fromEntries(Object.entries(doc.policy).map(([method, programs]) => [
method,
Array.isArray(programs)
? programs.map((program) => isPlainObject(program) &&
isPlainObject(program.script) &&
typeof program.script.source === 'string'
? {
...program,
script: new ScriptSource(program.script.source, program.script.language, program.script.module === true),
}
: program)
: programs,
]));
return SessionProfile({ ...doc, policy });
}
function mountDoc(entry) {
const doc = {};
if (entry.mode != null)
doc.mode = entry.mode;
if (entry.paths != null)
doc.paths = pathsDoc(entry.paths);
if (entry.commands != null)
doc.commands = commandsDoc(entry.commands);
return doc;
}
function pathsDoc(block) {
// A path shown twice answers with the weaker of its modes, and an
// entry without one states visibility only (it sets no mode), so the
// mapping keeps that answer for each path.
const show = new Map();
for (const e of block.show ?? []) {
const held = show.get(e.path) ?? null;
show.set(e.path, held === null ? e.mode : e.mode === null ? held : weakerMode(held, e.mode));
}
return {
hide: [...block.hide],
show: Object.fromEntries(show),
reasons: (block.reasons ?? []).map((g) => ({ patterns: [...g.patterns], reason: g.reason })),
};
}
function commandsDoc(block) {
const doc = {
ask: (block.ask ?? []).map(ruleDoc),
deny: (block.deny ?? []).map(ruleDoc),
};
if ('allow' in block && block.allow != null)
doc.allow = [...block.allow];
return doc;
}
function ruleDoc(rule) {
const commands = rule.commands ?? [];
const paths = rule.paths ?? [];
// A rule naming commands and paths is the mapping form, each command
// on the rule's paths; it reads back as one rule per command, which
// admits exactly what the one rule did.
if (commands.length > 0 && paths.length > 0) {
return {
reason: rule.reason,
commands: Object.fromEntries(commands.map((c) => [c, [...paths]])),
};
}
if (paths.length > 0)
return { reason: rule.reason, paths: [...paths] };
return { reason: rule.reason, commands: [...commands] };
}
//# sourceMappingURL=profile.js.map