UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

324 lines • 13.2 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 { 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