@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
185 lines • 6.96 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 { VFSName } from "../../types.js";
import { CacheKey, JobKey, MountKey, StateKey, VFSStateKey } from "./keys.js";
import { BLOB_REF_KEY, isSafeBlobPath } from "./utils.js";
class BlobAllocator {
blobs = {};
counters = new Map();
alloc(category) {
const i = this.counters.get(category) ?? 0;
this.counters.set(category, i + 1);
return `${category}/${String(i)}.bin`;
}
}
function isDict(v) {
return typeof v === 'object' && v !== null && !Array.isArray(v) && !(v instanceof Uint8Array);
}
/**
* The state as a JSON-safe manifest plus the bytes it referenced.
*
* The manifest IS the state, minus the four keys that hold bytes: those
* are rewritten with blob references and everything else rides through
* untouched. It is built that way round on purpose. An allowlist of keys
* silently dropped whatever it forgot from every tar snapshot while
* leaving in-memory snapshots (which never pass through here) green,
* which is exactly how installed CLIs went missing. A disk mount's files
* ride as host paths, read only when the tar is written.
*/
export function splitManifestAndBlobs(state) {
const a = new BlobAllocator();
const cache = state[StateKey.CACHE] ?? {};
const cacheEntries = cache[CacheKey.ENTRIES] ?? [];
const jobs = state[StateKey.JOBS] ?? [];
const mounts = state[StateKey.MOUNTS] ?? [];
const manifest = { ...state };
manifest[StateKey.HISTORY] = historyToManifest(state[StateKey.HISTORY], a);
manifest[StateKey.MOUNTS] = mounts.map((m) => mountToManifest(m, a));
manifest[StateKey.CACHE] = {
...cache,
[CacheKey.ENTRIES]: cacheEntries.map((e) => cacheEntryToManifest(e, a)),
};
manifest[StateKey.JOBS] = jobs.map((j) => jobToManifest(j, a));
return [manifest, a.blobs];
}
/** One cache entry with its data stashed as a blob reference. */
function cacheEntryToManifest(entry, a) {
const e = { ...entry };
const data = e[CacheKey.DATA];
if (data instanceof Uint8Array) {
const tarPath = 'cache/blobs/' + a.alloc('_cache');
a.blobs[tarPath] = data;
e[CacheKey.DATA] = { [BLOB_REF_KEY]: tarPath };
}
return e;
}
/** One job with its streams stashed as blob references. */
function jobToManifest(job, a) {
const j = { ...job };
for (const f of [JobKey.STDOUT, JobKey.STDERR]) {
const data = j[f];
if (data instanceof Uint8Array && data.byteLength > 0) {
const tarPath = 'jobs/blobs/' + a.alloc('_jobs');
a.blobs[tarPath] = data;
j[f] = { [BLOB_REF_KEY]: tarPath };
}
else if (data instanceof Uint8Array) {
j[f] = '';
}
}
return j;
}
function mountToManifest(mount, a) {
const idx = mount[MountKey.INDEX];
const ps = { ...mount[MountKey.VFS_STATE] };
const ptype = ps[VFSStateKey.TYPE];
const files = ps[VFSStateKey.FILES] ?? {};
if (ptype === VFSName.RAM || ptype === VFSName.OPFS) {
ps[VFSStateKey.FILES] = stashBlobs(files, a, `_ram${String(idx)}`, `mounts/${String(idx)}/files`);
}
else if (ptype === VFSName.DISK) {
const newFiles = {};
for (const [rel, data] of Object.entries(files)) {
const tarPath = `mounts/${String(idx)}/files/${rel}`;
a.blobs[tarPath] = data;
newFiles[rel] = { [BLOB_REF_KEY]: tarPath };
}
ps[VFSStateKey.FILES] = newFiles;
}
else if (ptype === VFSName.REDIS) {
ps[VFSStateKey.FILES] = stashBlobs(files, a, `_redis${String(idx)}`, `mounts/${String(idx)}/data`);
}
const out = {};
for (const [k, v] of Object.entries(mount)) {
if (k !== MountKey.VFS_STATE)
out[k] = v;
}
out[MountKey.VFS_STATE] = ps;
return out;
}
function stashBlobs(files, a, category, tarDir) {
const out = {};
for (const [k, data] of Object.entries(files)) {
const slot = a.alloc(category).split('/').pop() ?? '';
const tarPath = `${tarDir}/${slot}`;
a.blobs[tarPath] = data;
out[k] = { [BLOB_REF_KEY]: tarPath };
}
return out;
}
function historyToManifest(records, a) {
if (records === null || records === undefined)
return null;
if (!Array.isArray(records))
return records;
const out = [];
for (const r of records) {
const rd = { ...r };
for (const f of ['stdout', 'stdin', 'stderr']) {
const data = rd[f];
if (data instanceof Uint8Array && data.byteLength > 0) {
const tarPath = 'history/blobs/' + a.alloc('_history');
a.blobs[tarPath] = data;
rd[f] = { [BLOB_REF_KEY]: tarPath };
}
else if (data instanceof Uint8Array) {
rd[f] = '';
}
}
if ('tree' in rd)
rd.tree = nodeToManifest(rd.tree, a);
out.push(rd);
}
return out;
}
function nodeToManifest(node, a) {
if (!isDict(node))
return node;
const out = { ...node };
const data = out.stderr;
if (data instanceof Uint8Array && data.byteLength > 0) {
const tarPath = 'history/blobs/' + a.alloc('_history');
a.blobs[tarPath] = data;
out.stderr = { [BLOB_REF_KEY]: tarPath };
}
else if (data instanceof Uint8Array) {
out.stderr = '';
}
if (Array.isArray(out.children)) {
out.children = out.children.map((c) => nodeToManifest(c, a));
}
return out;
}
export function resolveManifest(manifest, blobReader) {
return resolve(manifest, blobReader);
}
function resolve(node, blobReader) {
if (isDict(node)) {
const keys = Object.keys(node);
if (keys.length === 1 && keys[0] === BLOB_REF_KEY) {
const path = node[BLOB_REF_KEY];
if (!isSafeBlobPath(path))
throw new Error(`Unsafe blob path in manifest: ${String(path)}`);
return blobReader(path);
}
const out = {};
for (const [k, v] of Object.entries(node))
out[k] = resolve(v, blobReader);
return out;
}
if (Array.isArray(node))
return node.map((v) => resolve(v, blobReader));
return node;
}
//# sourceMappingURL=manifest.js.map