@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
312 lines • 11.9 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 { drain } from "../../io/stream.js";
import { closeStream, openStream, serveRequests } from "../../rs/stream.js";
import { byteView, textView } from "../../shell/bytes.js";
import { VFSName } from "../../types.js";
import { concat } from "../../utils/bytes.js";
import { fileChunks, fileSize, regularChunks, stagedPath, writeStreamToFile } from "./fs.js";
import { MountKey, StateKey, VFSStateKey } from "./keys.js";
import { resolveManifest } from "./manifest.js";
import { BLOB_REF_KEY, isSafeBlobPath } from "./utils.js";
const ENC = new TextEncoder();
const DEC = new TextDecoder('utf-8', { fatal: false });
const MANIFEST_NAME = 'manifest.json';
// The name the archive goes by inside the tar engine.
const ARCHIVE = 'snapshot.tar';
/**
* The tar as one buffer, for a caller that needs bytes (an agents SDK that
* stores snapshots as buffers). A blob that is a host path is read through
* `regularChunks`, refused if a link has replaced it.
*/
export async function writeSnapshotTar(manifest, blobs, compress = null) {
const parts = [];
await pack(manifest, blobs, compress, (data) => {
parts.push(data);
return Promise.resolve();
});
return concat(parts);
}
export async function readSnapshotTar(data, compress = null, maxBytes) {
return unpack(once(data), null, compress, maxBytes);
}
/**
* Write manifest + blobs as a tar at `target`, streaming: a blob that is a
* host path (a disk mount's file) is read a chunk at a time, opened only
* while it is read. Returns the tar's size.
*/
export async function writeTar(target, manifest, blobs) {
const [writer, written] = fileWriter(target);
try {
await pack(manifest, blobs, null, (data) => writer.write(data));
await writer.close();
}
catch (error) {
await writer.abort(error);
await written.catch(() => undefined);
throw error;
}
return written;
}
/**
* Read a tar written by `writeTar` from the file at `source`, streaming.
* Each disk mount's files are extracted under `staging` and come back as
* host paths, so a restore copies them into place instead of holding them
* in memory; the caller removes the directory once the state is loaded.
*/
export async function readTar(source, staging, maxBytes) {
return unpack(fileChunks(source), staging, null, maxBytes);
}
/**
* Run mirage-rs's tar to its end, `serve` answering each request; a refusal
* throws in tar's own words.
*/
async function run(config, serve) {
const stream = await openStream('tar', config);
const errors = [];
const host = {
errors: {
append: (data) => {
errors.push(data);
return Promise.resolve();
},
},
serve,
};
try {
await drain(serveRequests(stream, host));
if (stream.status() !== 0) {
const message = DEC.decode(concat(errors)).trim();
throw new Error(message === 'tar: decoded input exceeds size limit'
? 'snapshot exceeds expanded size limit'
: message);
}
}
finally {
await closeStream(stream);
}
}
/** The manifest, then each blob, as tar's -c writes them to `write`. */
async function pack(manifest, blobs, compress, write) {
const entries = [
[MANIFEST_NAME, ENC.encode(JSON.stringify(manifest, null, 2))],
...Object.entries(blobs),
];
const taken = counts(entries.map(([name]) => memberName(name)));
for (const [name] of entries) {
if ((taken.get(memberName(name)) ?? 0) > 1)
throw new Error(`Blob paths name one member: ${name}`);
}
const members = [];
for (const [index, [name, data]] of entries.entries()) {
const size = typeof data === 'string' ? await fileSize(data) : data.byteLength;
members.push({ name: byteView(name), path: byteView(name), operand: index + 2, size });
}
const inputs = new Map();
try {
await run({
mode: 'create',
archive: ARCHIVE,
output: ARCHIVE,
codec: compress === 'gz' ? 'gzip' : null,
members,
}, async (request, data) => {
if (request.op === 'write')
await write(data);
if (request.op !== 'read')
return { reply: {} };
const fd = request.fd;
if (typeof request.operand === 'number') {
const blob = entries[request.operand - 2]?.[1] ?? new Uint8Array();
inputs.set(fd, typeof blob === 'string' ? regularChunks(blob) : once(blob));
}
const next = await inputs.get(fd)?.next();
if (next === undefined || next.done === true) {
inputs.delete(fd);
return { reply: { end: true } };
}
return { reply: {}, data: next.value };
});
}
finally {
for (const input of inputs.values())
await input.return(undefined);
}
}
/**
* Read a snapshot tar from `source` in one pass: the manifest first, then
* each blob it names, a disk mount's file written under `staging` when there
* is one (and held as well when the manifest names it elsewhere too), every
* other blob held in memory; a member it names no blob for is read past, and
* one stored twice is refused.
*/
async function unpack(source, staging, compress, maxBytes) {
if (maxBytes !== undefined && maxBytes < 0)
throw new Error('maxBytes must be non-negative');
let total = 0;
const held = new Map();
const staged = new Map();
const files = new Map();
let manifest = null;
let named = new Map();
let disk = new Map();
const begin = async (name) => {
if (held.has(name) || staged.has(name))
throw new Error(`Blob stored twice: ${name}`);
if (name === MANIFEST_NAME) {
held.set(name, []);
return;
}
if (manifest === null) {
manifest = manifestOf(held);
named = counts(blobPaths(manifest).map(memberName));
disk = counts(diskFileRefs(manifest).map(([, , blobPath]) => memberName(blobPath)));
}
if (!named.has(name))
return;
if (staging !== null && disk.has(name)) {
const target = await stagedPath(staging, name);
files.set(name, fileWriter(target));
staged.set(name, target);
}
if (!staged.has(name) || (named.get(name) ?? 0) > (disk.get(name) ?? 0))
held.set(name, []);
};
const serve = async (request, data) => {
if (request.op === 'read') {
const next = await source.next();
return next.done === true ? { reply: { end: true } } : { reply: {}, data: next.value };
}
if (request.op === 'stat')
return { reply: { kind: 'dir' } };
if ((request.op !== 'write' && request.op !== 'flush') || typeof request.path !== 'string') {
return { reply: {} };
}
const name = textView(request.path).replace(/^\/+/, '');
if (request.create === true || request.whole === true)
await begin(name);
if (maxBytes !== undefined) {
const copies = name === MANIFEST_NAME ? 1 : Math.max(1, named.get(name) ?? 0);
const amount = data.byteLength * copies;
if (amount > maxBytes - total)
throw new Error('snapshot exceeds expanded size limit');
total += amount;
}
held.get(name)?.push(data);
const file = files.get(name);
if (file === undefined)
return { reply: {} };
await file[0].write(data);
if (request.op === 'flush' || request.whole === true) {
files.delete(name);
await file[0].close();
await file[1];
}
return { reply: {} };
};
try {
await run({
mode: 'extract',
archive: ARCHIVE,
base: '',
codec: compress === 'gz' ? 'gzip' : null,
max_decoded_bytes: maxBytes ?? null,
}, serve);
}
finally {
for (const [writer, written] of files.values()) {
await writer.abort();
await written.catch(() => undefined);
}
await source.return(undefined);
}
manifest ??= manifestOf(held);
for (const [map, rel, blobPath] of diskFileRefs(manifest)) {
const path = staged.get(memberName(blobPath));
if (path !== undefined)
map[rel] = path;
}
return resolveManifest(manifest, (blobPath) => {
if (!isSafeBlobPath(blobPath))
throw new Error(`Unsafe blob path: ${String(blobPath)}`);
if (memberName(blobPath) === MANIFEST_NAME)
throw new Error('Manifest cannot reference itself as a blob');
const blob = held.get(memberName(blobPath));
if (blob === undefined)
throw new Error(`Manifest references missing blob: ${blobPath}`);
return concat(blob);
});
}
/** A writer into a new file at `path`, whose writes fail once the copy into the file does. */
function fileWriter(path) {
const { readable, writable } = new TransformStream();
const writer = writable.getWriter();
const written = writeStreamToFile(path, readable);
void written.catch((error) => writer.abort(error));
return [writer, written];
}
/** The name tar's -x gives the member a blob path was written under: the path without its empty and `.` components. */
function memberName(path) {
return path
.split('/')
.filter((part) => part !== '' && part !== '.')
.join('/');
}
/** The blob path of every reference in the manifest, each checked safe. */
function blobPaths(manifest) {
const paths = [];
resolveManifest(manifest, (path) => {
paths.push(path);
return new Uint8Array();
});
return paths;
}
/** How many times each name comes. */
function counts(names) {
const out = new Map();
for (const name of names)
out.set(name, (out.get(name) ?? 0) + 1);
return out;
}
async function* once(data) {
await Promise.resolve();
yield data;
}
/** The manifest, the archive's first member, parsed. */
function manifestOf(held) {
const raw = held.get(MANIFEST_NAME);
if (raw === undefined)
throw new Error(`${MANIFEST_NAME} missing or unreadable`);
return JSON.parse(DEC.decode(concat(raw)));
}
/**
* Every disk mount file's blob reference in the manifest, as the files map
* holding it, its key and its tar path: what a restore stages on disk.
*/
function diskFileRefs(manifest) {
const refs = [];
for (const mount of manifest[StateKey.MOUNTS] ?? []) {
const vfsState = mount[MountKey.VFS_STATE];
if (vfsState?.[VFSStateKey.TYPE] !== VFSName.DISK)
continue;
const files = vfsState[VFSStateKey.FILES] ?? {};
for (const [rel, ref] of Object.entries(files)) {
const blobPath = ref[BLOB_REF_KEY];
if (blobPath !== undefined)
refs.push([files, rel, blobPath]);
}
}
return refs;
}
//# sourceMappingURL=tar_io.js.map