@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
324 lines • 13.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 { PolicyError } from "../../policy/errors.js";
import { compileProgram, } from "../../policy/profile.js";
import { DEFAULT_PROCESS_PERMISSIONS, parseProcessPermissions, restrictProcesses, } from "../../process/config.js";
import { fromUnits, units } from "../../rs/document.js";
import { StreamRefusal } from "../../rs/errors.js";
import { admissionMatcher, rulesWire, visibilityFrom } from "../../rs/policy.js";
import { DEFAULT_VISIBILITY, weakerMode } from "../../types.js";
import { DEFAULT_PROFILE } from "./constants.js";
import { varsFromEntries } from "./session.js";
import { setCwd } from "./shell_dirs.js";
import { checkRules } from "./validate.js";
/**
* The profile a session is created from. A name is looked up as written; a
* profile object is itself; null picks `profiles.default` when the
* workspace defines one and leaves the session unrestricted otherwise.
* There is no inheritance chain: a profile is the whole document, so
* nothing is assembled from somewhere else before it is read. Throws
* PolicyError on a name the workspace does not define.
*/
export function resolveProfile(profiles, profile) {
if (profile === null || profile === undefined)
return profiles[DEFAULT_PROFILE] ?? null;
if (typeof profile !== 'string')
return profile;
const found = profiles[profile];
if (found === undefined)
throw new PolicyError(`unknown profile ${JSON.stringify(profile)}`);
return found;
}
function pathsWire(block) {
if (block == null)
return null;
return {
hide: block.hide.map(units),
show: (block.show ?? []).map((entry) => ({ path: units(entry.path), mode: entry.mode })),
reasons: (block.reasons ?? []).map((group) => ({
patterns: group.patterns.map(units),
reason: units(group.reason),
})),
};
}
function profileWire(profile) {
const block = profile.commands;
return {
commands: block == null ? null : rulesWire(block),
paths: pathsWire(profile.paths),
vars: profile.vars == null ? null : profile.vars.hide.map(units),
mounts: profile.mounts == null
? null
: [...profile.mounts].map(([prefix, entry]) => ({
prefix: units(prefix),
mode: entry.mode ?? null,
commands: entry.commands == null ? null : rulesWire(entry.commands),
paths: pathsWire(entry.paths),
})),
};
}
function ruleFrom(value) {
return {
reason: fromUnits(value.reason),
...(value.commands.length === 0 ? {} : { commands: value.commands.map(fromUnits) }),
...(value.paths.length === 0 ? {} : { paths: value.paths.map(fromUnits) }),
...(value.mount === null ? {} : { mount: fromUnits(value.mount) }),
};
}
function rulesFrom(value) {
if (value === null)
return null;
return {
allow: value.allow === null ? null : value.allow.map(fromUnits),
deny: value.deny.map(ruleFrom),
ask: value.ask.map(ruleFrom),
};
}
function reasonFrom(value) {
return { patterns: value.patterns.map(fromUnits), reason: fromUnits(value.reason) };
}
function pathsFrom(value) {
if (value === null)
return null;
const show = value.show.map((entry) => ({ path: fromUnits(entry.path), mode: entry.mode }));
const reasons = value.reasons.map(reasonFrom);
return {
hide: value.hide.map(fromUnits),
...(show.length === 0 ? {} : { show }),
...(reasons.length === 0 ? {} : { reasons }),
};
}
function profileFields(value) {
return {
commands: rulesFrom(value.commands),
paths: pathsFrom(value.paths),
vars: value.vars === null ? null : { hide: value.vars.map(fromUnits) },
mounts: value.mounts === null
? null
: new Map(value.mounts.map((mount) => {
const rules = rulesFrom(mount.commands);
const paths = pathsFrom(mount.paths);
return [
fromUnits(mount.prefix),
{
...(mount.mode === null ? {} : { mode: mount.mode }),
...(rules === null ? {} : { commands: { ask: rules.ask, deny: rules.deny } }),
...(paths === null ? {} : { paths }),
},
];
})),
};
}
function restrictMatching(base, inline) {
let value;
try {
value = admissionMatcher(null).ask('restrict', {
base: base === null ? null : profileWire(base),
inline: inline === null ? null : profileWire(inline),
});
}
catch (error) {
if (error instanceof StreamRefusal)
throw new PolicyError(error.message);
throw error;
}
return value === null ? null : profileFields(value);
}
function compilePlan(profile) {
return admissionMatcher(null).ask('compile', { profile: profileWire(profile) });
}
/**
* A profile with the inline document of one `createSession` added.
*
* The one rule about combining two documents: an inline document may
* add ask and deny rules and hides, never an allow list and never a
* script, and that holds even when there is no profile to add to. Modes
* take the weaker of the two, `cwd` and `env` are the inline document's
* when it states them (they are session presets, not permissions).
* Either side null returns the other unchanged; the profile's policy
* survives the merge, since the inline document can only add rules
* beside it.
*/
export function withInline(base, inline) {
if (inline === null)
return base;
const restriction = restrictMatching(base, inline);
if (inline.commandLimits != null)
throw new PolicyError('command_limits belong on the profile, not inline permissions');
if (inline.network != null)
throw new PolicyError('network belongs on the profile, not inline permissions');
if (inline.policy !== undefined && inline.policy !== null) {
throw new PolicyError('inline permissions may add ask and deny rules, not a policy; state one on the profile');
}
const own = base?.processes == null ? DEFAULT_PROCESS_PERMISSIONS : parseProcessPermissions(base.processes);
const processes = inline.processes == null
? own
: restrictProcesses(own, parseProcessPermissions(inline.processes));
if (base === null)
return inline.processes == null ? inline : { ...inline, processes };
return {
...restriction,
commandLimits: base.commandLimits ?? null,
processes,
network: base.network ?? null,
cwd: inline.cwd ?? base.cwd ?? null,
...(base.env != null || inline.env != null ? { env: { ...base.env, ...inline.env } } : {}),
...(base.policy != null ? { policy: base.policy } : {}),
};
}
/**
* The profile's policy programs by method, compiled onto the session.
* `name` is the profile's name, empty for a document passed without one;
* what a program reads as `ctx.profile`.
*
* @throws PolicyError - a program is still a path, which means it
* reached the workspace without passing the config loader that loads one.
*/
export function compileScripts(effective, name) {
const compiled = {};
for (const [method, programs] of Object.entries(effective.policy ?? {})) {
const entries = programs.map((program) => {
try {
return compileProgram(program, name);
}
catch (err) {
throw new PolicyError(`profile '${name}' ${err instanceof Error ? err.message : String(err)}`);
}
});
if (entries.length > 0)
compiled[method] = entries;
}
return compiled;
}
/** The session fields a profile compiles to. */
export function compileProfile(effective, name = '') {
const profile = name === '' ? null : name;
if (effective === null) {
return {
setup: { env: null, cwd: null },
visibility: DEFAULT_VISIBILITY,
policies: {
mountModes: null,
commands: null,
scripts: {},
commandLimits: null,
processes: DEFAULT_PROCESS_PERMISSIONS,
network: null,
},
hideReasons: [],
profile,
};
}
const plan = compilePlan(effective);
const commands = rulesFrom(plan.commands);
checkRules(commands);
const processes = effective.processes == null
? DEFAULT_PROCESS_PERMISSIONS
: parseProcessPermissions(effective.processes);
return {
setup: { env: effective.env ?? null, cwd: effective.cwd ?? null },
visibility: visibilityFrom(plan.visibility, processes.list),
policies: {
mountModes: plan.mount_modes.length === 0
? null
: new Map(plan.mount_modes.map(([prefix, mode]) => [fromUnits(prefix), mode])),
commands,
scripts: compileScripts(effective, name),
commandLimits: effective.commandLimits ?? null,
processes,
network: effective.network ?? null,
},
hideReasons: plan.hide_reasons.map(reasonFrom),
profile,
};
}
/**
* Stamp a compiled profile's narrowing onto a session: the fields no
* shell line can edit (the per-mount modes, hidden paths, show entries,
* hidden variables, hide reasons, the admission rules, the profile's
* script, the profile's name). Applied at creation and again
* whenever a stored record could carry a stale copy (the default
* session after hydration), so the document, not the store, is what an
* agent runs under.
*/
export function narrow(session, compiled) {
const policies = compiled.policies;
session.mountModes = policies.mountModes === null ? null : new Map(policies.mountModes);
session.visibility = compiled.visibility;
session.hideReasons = compiled.hideReasons;
session.commands = policies.commands;
session.scripts = policies.scripts;
session.profile = compiled.profile;
session.commandLimits = { ...policies.commandLimits };
session.processes = policies.processes;
session.network = policies.network;
}
/**
* The narrowing a session holds, as the compiled profile `narrow` stamps
* back unchanged: how a recorded session's restrictions land on another,
* the inline document and every later profile change kept. Mirrors
* Python `narrowing_of`.
*/
export function narrowingOf(session) {
return {
setup: { env: null, cwd: null },
visibility: session.visibility,
policies: {
mountModes: session.mountModes,
commands: session.commands,
scripts: session.scripts,
commandLimits: session.commandLimits,
processes: session.processes,
network: session.network,
},
hideReasons: session.hideReasons,
profile: session.profile,
};
}
/**
* Two sets of per-mount caps held at once: every prefix either one caps,
* at the weaker mode, so the result is never wider than either. Mirrors
* Python `stricter_modes`.
*/
export function stricterModes(a, b) {
if (a === null || b === null) {
const caps = a ?? b;
return caps === null ? null : new Map(caps);
}
const out = new Map(a);
for (const [prefix, mode] of b) {
const held = out.get(prefix);
out.set(prefix, held === undefined ? mode : weakerMode(held, mode));
}
return out;
}
/**
* Narrow a fresh session and seed its scratch state from the profile.
* A profile's env speaks the workspace `env` block's grammar, so a bare
* string is exported: seeding it plain left `$TOKEN` expanding while
* every command, CLI and guest runtime in the profiled session saw
* nothing, since all three read `envSnapshot` and that is the exported
* set. A managed entry lands as its pointer, fetched when a line names
* it. The cwd is where the session starts; both are the agent's to
* change afterwards, which is why hydration keeps the stored ones and
* re-stamps only `narrow`.
*/
export function applyProfile(session, compiled) {
narrow(session, compiled);
if (compiled.setup.env != null)
Object.assign(session.vars, varsFromEntries(compiled.setup.env));
if (compiled.setup.cwd !== null)
setCwd(session, compiled.setup.cwd);
}
//# sourceMappingURL=resolve.js.map