UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

185 lines • 6.96 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 { 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