UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

184 lines • 9.34 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 { OPERAND_CONDITIONS, READ_FAILURES } from "./constants.js"; import { errorVirtualPath, fsError, fsStrerror, virtualOf } from "./fs.js"; import { posixPhrase } from "./posix.js"; import { encodeText } from "../shell/bytes.js"; import { dropTrailingSegments, respellOne } from "../utils/path.js"; import { quotesOperands, shellQuote, shellQuoteAlways } from "../utils/quote.js"; import { rstripSlash } from "../utils/slash.js"; // Re-spell a reported path the way its operand was typed. Backends name paths // in virtual space, but a command quotes the operand as the user wrote it: // `cd /data && mkdir -p f.txt/sub` reports 'f.txt', not '/data/f.txt'. The path // an error names is the operand itself, an ancestor of it (mkdir -p blames the // component of the chain it tripped on), or something under it, so all three // are rebased onto rawPath. An absolute operand rebases to itself, which is why // this is a no-op for most invocations. Mirrors Python's operand_spelling. export function operandSpelling(path, operand) { const virtual = operand.virtual; const raw = operand.rawPath ?? virtual; if (raw === virtual) return path; if (path === virtual) return raw; const base = rstripSlash(virtual); if (path.startsWith(base + '/')) return respellOne(path, virtual, raw); const trimmed = rstripSlash(path); if (base.startsWith(trimmed + '/')) { return dropTrailingSegments(raw, segments(base).length - segments(trimmed).length); } return path; } function segments(path) { return path.split('/').filter((part) => part !== ''); } const CANNOT_OPEN = 'cannot open {quoted} for reading: {strerror}'; // How a failed operand is worded by the commands that name the step that // failed instead of printing `<cmd>: <name>: <strerror>`. An entry is // [opening, reading]: the line for a name the command could not open, and // for one it opened that then refused the read (READ_FAILURES). null keeps // the plain line for that step, which is also the choice wherever the // reference line drops the name (`base64: read error`, `fmt: read error`): // mirage words a step that way only while it still says which operand // failed. `{quoted}` is the name always quoted, `{shown}` quoted only when it // needs it, `{bare}` as typed. Measured on debian:stable-slim, a directory // read on tmpfs: overlayfs answers a directory's read with EINVAL, so a tac // there says `read error: Invalid argument`. Mirrors Python's // FAILURE_WORDING. export const FAILURE_WORDING = new Map([ ['csplit', [CANNOT_OPEN, null]], ['du', ['cannot access {quoted}: {strerror}', null]], ['find', ['{quoted}: {strerror}', '{quoted}: {strerror}']], ['fmt', [CANNOT_OPEN, null]], ['head', [CANNOT_OPEN, 'error reading {quoted}: {strerror}']], ['ls', ['cannot access {quoted}: {strerror}', null]], [ 'mkdir', [ 'cannot create directory {quoted}: {strerror}', 'cannot create directory {quoted}: {strerror}', ], ], ['rev', ['cannot open {bare}: {strerror}', null]], ['rm', ['cannot remove {quoted}: {strerror}', 'cannot remove {quoted}: {strerror}']], ['rmdir', ['failed to remove {quoted}: {strerror}', 'failed to remove {quoted}: {strerror}']], ['sed', ["can't read {bare}: {strerror}", 'read error on {bare}: {strerror}']], ['split', [CANNOT_OPEN, null]], ['stat', ['cannot statx {quoted}: {strerror}', 'cannot statx {quoted}: {strerror}']], ['tac', ['failed to open {quoted} for reading: {strerror}', '{shown}: read error: {strerror}']], ['tail', [CANNOT_OPEN, 'error reading {quoted}: {strerror}']], ['touch', ['cannot touch {quoted}: {strerror}', 'cannot touch {quoted}: {strerror}']], [ 'truncate', [ 'cannot open {quoted} for writing: {strerror}', 'cannot open {quoted} for writing: {strerror}', ], ], ['tsort', [null, '{shown}: read error: {strerror}']], ['uniq', [null, 'error reading {quoted}: {strerror}']], ]); // wc and du vet every name the way their --files0-from reader does, and // refuse an empty one in these words before any open could answer ENOENT // for it. Mirrors Python's ZERO_LENGTH_NAME. export const ZERO_LENGTH_NAME = 'invalid zero-length file name'; const VETS_EMPTY_NAMES = new Set(['du', 'wc']); // The command's own template for this failure, null for the plain line: no // entry, no template for the step, or standard input, whose `-` line is the // one a command prints when it closes a stdin it could not read. function stepWording(cmdName, label, code) { const wording = FAILURE_WORDING.get(cmdName); if (wording === undefined || label === '-') return null; return code !== undefined && READ_FAILURES.has(code) ? wording[1] : wording[0]; } // The stderr line for one failed path operand, spelled as typed // (PathSpec.rawPath). Byte-identical with the executor chokepoint and the // Python fs_error_line. Used by read-family commands that keep processing // remaining operands after one fails, where the caller holds the operand. // A command in SHELL_QUOTED_COMMANDS reports the operand shell-quoted when // it needs it ('*.txt'); every other command reports it bare. A command in FAILURE_WORDING says which step failed instead. export function fsErrorLine(cmdName, path, err) { const code = err.code; const typed = virtualOf(path); if (typed === '' && VETS_EMPTY_NAMES.has(cmdName)) return `${cmdName}: ${ZERO_LENGTH_NAME}\n`; const strerror = fsStrerror(err); const template = stepWording(cmdName, typed, code); if (template !== null && strerror !== null) { // One pass, so a name that spells a placeholder is never substituted. const values = { quoted: shellQuoteAlways(typed), shown: shellQuote(typed), bare: typed, strerror, }; const line = template.replace(/\{(quoted|shown|bare|strerror)\}/g, (_, key) => { return values[key] ?? ''; }); return `${cmdName}: ${line}\n`; } const label = quotesOperands(cmdName) ? shellQuote(typed) : typed; if (strerror !== null) return `${cmdName}: ${label}: ${strerror}\n`; return `${cmdName}: ${label}\n`; } // Re-say another command's failed-operand line in `cmdName`'s voice. A // command that reads its operands through another one (the cross-mount // stream strategy fetches each with cat) holds that command's rendered // line, not the error. When the line is the fetch command's own // fsErrorLine for `operand`, it is rendered again from the strerror it // names, so the prefix, the quoting and the step wording are all the real // command's; any other line only has its prefix swapped. Mirrors Python's // revoice_fs_error_line. export function revoiceFsErrorLine(line, fromCmd, cmdName, operand) { const prefix = `${fromCmd}: `; if (!line.startsWith(prefix)) return line; const strerror = line.slice(line.lastIndexOf(': ') + 2); const code = [...OPERAND_CONDITIONS].find((c) => posixPhrase(c) === strerror); if (code !== undefined) { const err = fsError(operand, code); if (fsErrorLine(fromCmd, operand, err) === `${line}\n`) { return fsErrorLine(cmdName, operand, err).replace(/\n$/, ''); } } return `${cmdName}: ${line.slice(prefix.length)}`; } // The chokepoint variant of fsErrorLine for callers that only hold the // error, byte-identical with Python's format_fs_error: the path is // recovered from the error and, when `paths` is supplied, rewritten to the // as-typed spelling (PathSpec.rawPath) so a relative argument is reported // as typed. Shared by the single-mount and cross-mount // chokepoints; takes a structural shape to avoid importing PathSpec (no // import cycle). export function formatFsError(cmdName, err, paths) { const strerror = fsStrerror(err); const vpath = errorVirtualPath(err); const spelled = paths?.find((p) => p.virtual === vpath)?.rawPath ?? vpath; let line; if (strerror !== null) { line = fsErrorLine(cmdName, spelled, err); } else { // A message that already carries the `<cmd>: ` prefix (many generic // commands throw a fully formatted string, e.g. `uniq: invalid // count`) is emitted verbatim so the prefix is not doubled. const message = err instanceof Error ? err.message : String(err); line = message.startsWith(`${cmdName}: `) ? `${message}\n` : `${cmdName}: ${message}\n`; } return encodeText(line); } //# sourceMappingURL=render.js.map