UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

204 lines • 7.44 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 { ByteSpool } from "../io/spool.js"; import { chunks } from "../io/cooperative.js"; import { OutputStream } from "../io/stream.js"; import { FD_BOTH, FD_CLOSE } from "./constants.js"; import { SharedInput } from "../io/async_line_iterator.js"; import { JobConsole, Terminal } from "./console/index.js"; import { ebadf } from "../errors/fs.js"; import { RedirectKind } from "./types.js"; import { encodeText } from "./bytes.js"; /** * The first descriptor outside the signed 32-bit range, or null. Both slots count: the * descriptor a redirect claims (`3>f`, `3<f`, `3>&1`, `3>&-`) and the one * it duplicates from (`>&3`, `<&3`, `2>&3`). `&>`'s FD_BOTH and `>&-`'s * FD_CLOSE are the two sentinels the parser spells with -1, and neither is * a descriptor. */ export function unsupportedDescriptor(redirects) { for (const r of redirects) { // An ambiguous redirect (`3>&word`) is skipped: bash refuses it in its // own words before it judges the descriptor, and so does the installer. if (r.kind === RedirectKind.AMBIGUOUS) continue; if (!(r.fd >= 0 && r.fd < 2 ** 31) && r.fd !== FD_BOTH) return r.fd; if (typeof r.target === 'number' && !(r.target >= 0 && r.target < 2 ** 31) && r.target !== FD_CLOSE) { return r.target; } } return null; } /** Bash's error for a closed descriptor, without the line-number prefix. */ export function badDescriptorLine(fd) { return encodeText(`${String(fd)}: Bad file descriptor\n`); } /** * Standard input that fails on its first read with EBADF. bash opens a * command whose stdin is closed (`<&-`) or duplicated from a write-only * descriptor (`0<&1`) all the same; the descriptor exists, and only a * read of it fails. A command that never reads (`true 0<&1`) succeeds, * and one that does reports `<cmd>: -: Bad file descriptor` and exits 1, * which is what the chokepoint renders from the error this throws. */ export function unreadableStdin() { return { [Symbol.asyncIterator]: () => ({ next: () => Promise.reject(ebadf('-')) }), }; } export class FileDescription { scope; append; pipe; opened = false; offset = 0; source = null; emit = null; /** Settles when the last write through this description has. */ writing = Promise.resolve(); constructor(scope, append = false, pipe = false) { this.scope = scope; this.append = append; this.pipe = pipe; } } export class FileInput extends SharedInput { description; constructor(description, data) { super(data, null, true); this.description = description; } dup() { return this; } } /** * The stdout or stderr a level was given rather than opened. A descriptor * copied from it (`3>&1`, `exec 3>&1`) keeps naming it after the level * rebinds its own (`3>&1 >f`), as bash's copy keeps the open file * description. Mirrors Python's Inherited. */ export class Inherited { owner; channel; constructor(owner, channel) { this.owner = owner; this.channel = channel; } } /** * What one level's command wrote, in order, for the level to route. A chunk * on a channel goes through the level's descriptor table; one written to a * stream another level owns stays in place on its way up to that level, so it * lands among the bytes written around it. Mirrors Python's Recorder. */ export class Recorder extends JobConsole { chunks = []; owner = Symbol('recorder'); storage = new ByteSpool(); writing = Promise.resolve(); pending = 0; /** Whether a write is under way or recorded bytes wait to be taken. Mirrors Python's busy. */ get busy() { return this.pending > 0 || this.chunks.length > 0; } async record(key, data) { this.pending++; const before = this.writing; let done = () => undefined; this.writing = new Promise((resolve) => { done = resolve; }); try { await before; const start = this.storage.size; await this.storage.append(data); const last = this.chunks.at(-1); if (last?.[0] === key) last[2] += data.length; else this.chunks.push([key, start, data.length]); } finally { this.pending--; done(); } } async emit(channel, data) { await this.record(channel, data); } async emitTo(stream, data) { await this.record(stream instanceof Inherited ? stream : stream.channel, data); } /** * Drop what was recorded and never taken, for a level that failed before * it routed its output; a late job write starts afresh. Mirrors Python. */ async close() { await this.writing; this.chunks.splice(0); const storage = this.storage; this.storage = new ByteSpool(); try { await storage.close(); } finally { await super.close(); } } async take() { await this.writing; const entries = this.chunks.splice(0), storage = this.storage; this.storage = new ByteSpool(); if (storage.size <= storage.memoryLimit) { try { const data = await storage.read(0, storage.size); return entries.map(([key, start, size]) => [key, data.slice(start, start + size)]); } finally { await storage.close(); } } let remaining = entries.length; const release = async () => { if (--remaining === 0) await storage.close(); }; return entries.map(([key, start, size]) => [ key, new OutputStream(storage.stream(start, size), release), ]); } } /** * Send bytes written to a stream another level owns toward it: up through * the sink, or the enclosing level's recorder when the level returns its * output as a value or writes to a terminal of its own (a line's, a * substitution's), which owns no stream above it. A console that keeps no * streams takes them on their channel. False when there is nowhere above. * `enclosing` is the enclosing level's recorder, its caller's. Mirrors * Python's deliver. */ export async function deliver(sink, stream, data, enclosing) { const target = sink !== null && !(sink instanceof Terminal) ? sink : enclosing; if (target === null) return false; for await (const part of chunks(data)) await target.emitTo(stream, part); return true; } //# sourceMappingURL=descriptors.js.map