@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
193 lines • 9.24 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 { Limit, OnExceed } from "../../types.js";
import { compareCodePoints } from "../../utils/sort.js";
const DEFAULT_MAX_LINES = 2000;
// Null prototype: command names are script-controlled, so a name like
// `toString` must fall through to the fallback instead of resolving an
// `Object.prototype` member as a limit.
export const DEFAULT_COMMAND_LIMITS = Object.assign(Object.create(null), Object.fromEntries(['cat', 'grep', 'rg', 'head', 'tail'].map((name) => [
name,
new Limit({ maxLines: DEFAULT_MAX_LINES }),
])));
/** The line's time limit when no `@line` entry names one. Mirrors Python's LINE_TIMEOUT_SECONDS. */
export const LINE_TIMEOUT_SECONDS = 600;
/**
* The entry naming every command in a limit table, a group spelled with the
* `@` a runtime's captures use for theirs, so no command name can clash.
* Mirrors Python's ALL_COMMANDS.
*/
export const ALL_COMMANDS = '@all';
/** The entry naming the whole line, whose `timeoutSeconds` is its one time limit. Mirrors Python's LINE. */
export const LINE = '@line';
/** What a line past its time limit is called: `line: timed out after Ns`. Mirrors Python's LINE_NAME. */
export const LINE_NAME = 'line';
/**
* A command's own entry in a limit table, or null. Command names are
* script-controlled, so `toString` must not resolve an `Object.prototype`
* member of a table built with `{...}`.
*/
export function ownLimit(table, name) {
return Object.hasOwn(table, name) ? (table[name] ?? null) : null;
}
function entries(table, name) {
return [ownLimit(table, name), ownLimit(table, ALL_COMMANDS)];
}
/**
* Resolve one command's limit, the one precedence engine.
*
* Field by field, from the most specific layer that sets it: the profile's
* entry for the command, then its `@all` entry for every command, then the
* mount's two, the workspace's two, the command's own default and the
* built-in table; on a mount, its backend's default (`walkLimit`) last. A layer that names one bound inherits the rest. A command across
* several mounts resolves on each and keeps the tightest bound. Mirrors
* Python's resolve_limit.
*/
export function resolveLimit(name, mounts = [], commandDefault = null, mountOverride = null, workspaceLimits = {}, profileLimits = {}) {
const above = [...entries(profileLimits, name), mountOverride];
const below = [
...entries(workspaceLimits, name),
commandDefault,
DEFAULT_COMMAND_LIMITS[name] ?? null,
];
if (mounts.length === 0)
return Limit.layered([...above, ...below]);
return Limit.aggr(mounts.map((m) => Limit.layered([
...above,
m.commandLimits.get(name) ?? null,
m.commandLimits.get(ALL_COMMANDS) ?? null,
...below,
m.defaultLimit,
])));
}
/**
* How many files one walk lists on `mount`, -1 for no cap: the `walkLimit`
* of `command`, resolved as resolveLimit resolves every bound; without a
* command, the one every command shares (the `*` entries), which a glob
* counts against. A layer naming the bound as null lifts it. Mirrors
* Python's walk_bound.
*/
export function walkBound(mount, command = null, commandDefault = null, workspaceLimits = {}, profileLimits = {}) {
const found = resolveLimit(command ?? ALL_COMMANDS, [mount], commandDefault, null, workspaceLimits, profileLimits);
return found?.walkLimit ?? -1;
}
/**
* Resolve producer facts for command output guards and dispatch timeouts,
* through the same layers as resolveLimit on the mounts its prefixes name.
* Mirrors Python's resolve_producer.
*/
export function resolveProducer(producer, mountAt, workspaceLimits = {}, profileLimits = {}) {
if (producer.command === '')
return null;
const mounts = producer.prefixes
.map((prefix) => mountAt(prefix))
.filter((m) => m !== null);
return resolveLimit(producer.command, mounts, producer.declared, null, workspaceLimits, profileLimits);
}
// Each Limit field as config spells it.
const WIRE_FIELDS = [
['max_lines', 'maxLines'],
['max_bytes', 'maxBytes'],
['timeout_seconds', 'timeoutSeconds'],
['walk_limit', 'walkLimit'],
['on_exceed', 'onExceed'],
];
/** The canonical snake_case command-limit mapping used by config and stored sessions. */
export function parseCommandLimits(raw) {
if (raw === undefined || raw === null)
return {};
if (typeof raw !== 'object' || Array.isArray(raw))
throw new Error('command_limits must be a mapping');
const result = Object.create(null);
for (const [name, value] of Object.entries(raw)) {
if (value === null || typeof value !== 'object' || Array.isArray(value))
throw new Error(`command_limits.${name} must be a mapping`);
const block = value;
for (const key of Object.keys(block)) {
if (!['max_lines', 'max_bytes', 'timeout_seconds', 'walk_limit', 'on_exceed'].includes(key))
throw new Error(`command_limits.${name}: unknown field ${key}`);
}
for (const key of ['max_lines', 'max_bytes', 'timeout_seconds', 'walk_limit']) {
const v = block[key];
if (v != null && typeof v !== 'number')
throw new Error(`command_limits.${name}.${key} must be a number or null`);
}
if (block.on_exceed !== undefined &&
block.on_exceed !== OnExceed.ERROR &&
block.on_exceed !== OnExceed.TRUNCATE)
throw new Error(`command_limits.${name}.on_exceed must be truncate or error`);
const init = {};
for (const [key, field] of WIRE_FIELDS) {
if (key in block)
Object.assign(init, { [field]: block[key] });
}
result[name] = new Limit(init);
}
return result;
}
/**
* A limit table as config spells it, each entry with only the fields it names
* (a null among them), so a stored entry still inherits the rest. Mirrors
* Python's command_limits_to_json.
*/
export function commandLimitsToJSON(limits) {
return Object.fromEntries(Object.entries(limits).map(([name, limit]) => [
name,
Object.fromEntries(WIRE_FIELDS.filter(([, field]) => limit.fieldsSet.has(field)).map(([key, field]) => [
key,
field === 'onExceed' ? limit.onExceed : limit[field],
])),
]));
}
/**
* The whole line's deadline, the one time limit a line runs under: the
* `@line` entry's `timeoutSeconds`, the profile's over the workspace's, else
* `LINE_TIMEOUT_SECONDS`. An entry naming it as null or 0 lifts it. Mirrors
* Python's line_timeout.
*/
export function lineTimeout(workspaceLimits = {}, profileLimits = {}) {
const found = Limit.layered([ownLimit(profileLimits, LINE), ownLimit(workspaceLimits, LINE)]);
if (!found?.fieldsSet.has('timeoutSeconds'))
return LINE_TIMEOUT_SECONDS;
return found.timeoutSeconds !== null && found.timeoutSeconds > 0 ? found.timeoutSeconds : null;
}
/**
* Refuse a table that names time where no time limit applies. A line runs
* under one time limit, the `@line` entry's `timeoutSeconds`; no command or
* op entry takes one, and the `@line` entry takes nothing else. A mount
* names no line, since a line spans mounts. A key is a command or one of the
* two groups, `@all` and `@line`. Mirrors Python's check_command_limits.
*/
export function checkCommandLimits(limits, where, takesLine) {
for (const [name, limit] of Object.entries(limits)) {
if (name !== ALL_COMMANDS && name !== LINE && (name === '*' || name.startsWith('@'))) {
throw new Error(`${where}: command_limits['${name}']: not a command or a group; the groups are '${ALL_COMMANDS}' and '${LINE}'`);
}
if (name !== LINE) {
if (limit.fieldsSet.has('timeoutSeconds')) {
throw new Error(`${where}: command_limits['${name}'].timeout_seconds: a line has one time limit, set it on '${LINE}'`);
}
continue;
}
if (!takesLine) {
throw new Error(`${where}: command_limits['${LINE}']: a line spans mounts, set it on the workspace or a profile`);
}
const extra = [...limit.fieldsSet].filter((field) => field !== 'timeoutSeconds');
if (extra.length > 0) {
const named = extra.map((field) => field.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`));
throw new Error(`${where}: command_limits['${LINE}'] takes only timeout_seconds, not ${named.sort(compareCodePoints).join(', ')}`);
}
}
}
//# sourceMappingURL=output_cap.js.map