UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

169 lines • 6.86 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 { fsError } from "../../errors/fs.js"; import { posixPhrase } from "../../errors/posix.js"; import { readerOf } from "../files.js"; import { posixNormpath, resolvePath } from "../../utils/path.js"; import { encodeBase64 } from "../../utils/base64.js"; /** * Version stamp for one file's stored bytes. * * @param content The bytes to stamp. * @returns A base64url digest, matching the Python tracker's stamp. */ export async function fingerprint(content) { const digest = await crypto.subtle.digest('SHA-256', content); return encodeBase64(new Uint8Array(digest)) .replaceAll('+', '-') .replaceAll('/', '_') .replaceAll('=', ''); } async function readBytes(vfs, path) { return (await vfs.read(path, { raw: true })).slice(); } export class FileVersionTracker { vfs; enabled; readVersions = new Map(); editVersions = new Map(); writes = new Map(); seen = new Set(); /** * @param vfs The file API to read and write through, run as the * session whose reads are tracked. * @param enabled False serves every call unchecked. */ constructor(vfs, enabled = true) { this.vfs = vfs; this.enabled = enabled; } /** * This tracker read and written through `vfs`: one call's own view of * the session's files, which records what it sees where every other * call of the session finds it. */ through(vfs) { return Object.create(this, { vfs: { value: vfs } }); } // The stamp key for a path: one key per file, not per spelling. // readFile and writeFile follow the namespace symlink table, so // `/alias` and `/target` are the same file. Keying by the caller's // spelling would give each its own stamp, and an edit that arrived // through the other name would find no prior version and skip the // staleness check entirely. The dots go after the links they sit behind, // so `/w/./a.txt` and `/w/a.txt` share a stamp while `link/..` climbs from // where the link leads. key(path) { const reader = readerOf(this.vfs); if (!reader.linked) return resolvePath(path, '/'); const inner = posixNormpath(reader.follow(path)); return posixNormpath(reader.follow(resolvePath(inner, '/'))); } async currentVersion(path) { if (!(await this.vfs.exists(path))) return null; return fingerprint(await readBytes(this.vfs, path)); } async assertVersion(path, expected) { if ((await this.currentVersion(path)) !== expected) { // The tool adapters pass the message on whole, so it says what happened. throw fsError(path, 'STALE_WRITE', `${path}: ${posixPhrase('STALE_WRITE')}`); } } // Stamp what a later read will return, not the bytes handed in. A // mount that does not store writes verbatim answers with something // else, so stamping the input would make the very next write or edit // look stale with nobody having touched the file. async recordWrite(path, key) { if (!this.enabled) return; this.writes.set(key, (this.writes.get(key) ?? 0) + 1); const version = await this.currentVersion(path); if (version === null) this.readVersions.delete(key); else this.readVersions.set(key, version); this.editVersions.delete(key); } /** * Whether a write may overwrite the file: the agent was shown all of it, * or wrote all of it, since this tracker started, or nothing is checked. * A read of a few lines does not count, so a write never replaces lines * the agent did not see. */ hasRead(path) { return !this.enabled || this.seen.has(this.key(path)); } /** Record that the agent was shown all of the file. */ markSeen(path) { if (this.enabled) this.seen.add(this.key(path)); } // The bytes a read fetched may predate a write that lands while it is // in flight, so such a read fetches once more: the agent is never shown // bytes older than a write it already saw finish. A write that lands // during the second fetch too leaves the stamp of what was shown, and // the next write is refused as stale. async read(path) { const key = this.key(path); const writes = this.writes.get(key); let content = await readBytes(this.vfs, path); if (!this.enabled) return content; let version = await fingerprint(content); if (this.writes.get(key) !== writes) { content = await readBytes(this.vfs, path); version = await fingerprint(content); } this.readVersions.set(key, version); return content; } async readForEdit(path) { const content = await readBytes(this.vfs, path); if (!this.enabled) return content; const key = this.key(path); const version = await fingerprint(content); const readVersion = this.readVersions.get(key); if (readVersion !== undefined && readVersion !== version) { throw fsError(path, 'STALE_WRITE', `${path}: ${posixPhrase('STALE_WRITE')}`); } this.editVersions.set(key, version); return content; } async write(path, content) { const key = this.key(path); if (this.enabled) { const readVersion = this.readVersions.get(key); if (readVersion !== undefined) await this.assertVersion(path, readVersion); } await this.vfs.write(path, content); await this.recordWrite(path, key); if (this.enabled) this.seen.add(key); } async writeEdit(path, content) { const key = this.key(path); if (this.enabled) { const editVersion = this.editVersions.get(key); if (editVersion !== undefined) await this.assertVersion(path, editVersion); } await this.vfs.write(path, content); await this.recordWrite(path, key); } } //# sourceMappingURL=file_version.js.map