UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

396 lines • 19.1 kB
import type { OpenMode } from './handles/index.ts'; import { MountCore } from '../mount/core.ts'; import { type MountResolver } from './resolver.ts'; import type { BridgeDispatchFn } from './types.ts'; import { type WorkspaceBinding } from './binding.ts'; import type { Context } from '../commands/config.ts'; import { type SetAttrFields } from '../types.ts'; import type { VFSEntry, VFSStat } from './types.ts'; /** * Whether a listing row is one the file adapter did not classify. * * Its size-0 non-directory shape is a placeholder, not an answer: a * guest that needs the entry's kind, size or stamp must ask the mount * with a stat of its own, which also reports whatever failed. A * slash-marked directory and a link carry no mode either, but their * kind is known. */ export declare function isUnclassified(entry: VFSEntry | VFSStat): boolean; /** * One file a guest holds open: the run's core handle on it, the position * its reads and writes move, and what its open asked for. Nothing the guest * reads or writes stays here: the core holds it (`RuntimeFiles.open`), so * every entry point judges, lands and keeps an open file one way. A stream * and a descriptor over one open share this, and with it the position, as * POSIX shares an open file description. Mirrors Python's `Descriptor`. */ export declare class Descriptor { readonly files: RuntimeFiles; readonly fd: number; readonly mode: OpenMode; readonly opened: string; readonly directory: boolean; readonly inode: string; pos: number; /** * @param files the adapter whose core the handle is on. * @param fd the core's handle. * @param mode what the open asked for. * @param opened the path it was opened at, the name a file object reports. * @param directory it names a directory, which it may not read. * @param inode where the links on `opened` led as it opened, which names * its inode however the file is renamed or the link changed since. */ constructor(files: RuntimeFiles, fd: number, mode: OpenMode, opened: string, directory?: boolean, inode?: string); /** The path its file has now; a rename moves it. */ get path(): string; /** The file's length as the handle holds it, its writes counted. */ size(): Promise<number>; /** Read at an explicit offset without moving the position. */ pread(offset: number, size: number): Promise<Uint8Array>; /** * Read from the position, advancing it by what was read; a negative or * null size reads to the end, a chunk at a time. */ read(size?: number | null): Promise<Uint8Array>; /** Write at an explicit offset without moving the position; an appending handle writes at its end. */ pwrite(offset: number, data: Uint8Array): Promise<void>; /** * Write at the position, advancing it past what was written; an appending * handle first moves the position to its end, as O_APPEND does, so a * write that fails to find the end leaves nothing written. */ write(data: Uint8Array): Promise<void>; /** * Move the position, POSIX whence numbering; null when the whence is * unknown or the target would be negative (the position is then * untouched). */ seek(offset: number, whence: number): Promise<number | null>; /** Set the file's length: a shrink drops bytes, growth reads zeros. */ truncate(size: number): Promise<void>; /** Land what it wrote, as fsync(2) does. */ land(): Promise<void>; /** Land what it wrote and let the core's handle go. */ close(): Promise<void>; } /** * The mount-facing op vocabulary a sandboxed runtime encodes into. * * One instruction set (read/write/append/stat/readdir/create/truncate/ * unlink/mkdir/rmdir/rename/symlink/readlink/setattr), one routing * table, one place that knows an append may have to become a * whole-file write. The last three reach the name plane rather than a * backend, which is what lets a guest create a link or stamp a time on * a mount whose store has neither. Encoders hold one of these; they * never inherit it, because a monty encoder is the binding's own `os` * callback and a quickjs encoder is a table of host functions. * * The surface is async, unlike Python's: a JS guest either suspends at * the call (quickjs asyncify) or records the mutation and replays it * after the run (pyodide), so nothing here has to block a worker * thread the way the Python runtimes do. * * A file the guest opens is a handle on the run's `MountCore` (`open`, * `pread`, `pwrite`, `fstat`, `ftruncate`, `fsync`, `close`), the * open-file layer every entry point shares: it holds what the guest read, * keeps what it wrote until a close or a sync, lands it in the order the * writes were made, and keeps a file whose name went for the handles open * on it. The guest's own table keeps only the descriptor number, its * position and its flags. A removal, a rename, a truncate and a change of * times by path go through the core too, so the files open on that path * follow them. Mirrors Python's `RuntimeFiles`. */ export declare class RuntimeFiles { readonly bridge: BridgeDispatchFn; private readonly resolve; private readonly core; private readonly signal; private readonly live; private readonly dispatch; private readonly resolver; private readonly noAppend; /** * @param bridge the bridge every op rides, over the run's files; it also * answers the raw calls a guest makes past the typed methods (the * Pyodide worker's xattr and alias checks). * @param resolver the workspace mount routing table; the default answers * no mounts, so routing questions answer null. * @param resolve where the namespace's links lead a path; null while the * workspace keeps no links. * @param core the open-file core the run's files open on; null opens * nothing. * @param signal the run's, which stops a core op once it aborts. * @param live the signals of the guest calls on the core now, which its * every dispatch checks; a call with none (a run's end landing what it * wrote) passes, however the run ended. */ constructor(bridge: BridgeDispatchFn, resolver?: MountResolver, resolve?: ((path: string) => string) | null, core?: MountCore | null, signal?: AbortSignal | null, live?: AbortSignal[]); /** * The file adapter every engine builds for one run. * * Over the run's files (`ctx.vfs`, whose calls are the program's own), * the binding's routing table and the namespace's links, each replaying * the run's context, which `Runtime.execute` set up, for a guest that * calls back from elsewhere. A run with no files rides the binding's * guest bridge (`WorkspaceBinding.overBridge`, the Pyodide worker's). * * @param ctx the run's context. * @param binding the runtime's binding; null outside a workspace, which * has no adapter. */ static of(ctx: Context, binding: WorkspaceBinding | null): RuntimeFiles | null; /** A live guest view that stops issuing operations once its run is cancelled. */ withSignal(signal: AbortSignal): RuntimeFiles; /** * Run one op on the run's open-file core; EBADF when this adapter opens * nothing. An op that only lands what the guest already wrote (`lands`) * runs however the run ended, as the run's end would land it anyway * (`runLanding`), so a cancelled guest call under way beside it stops only * itself. */ private onCore; /** The path the core's handle `fd` is on now; EBADF for any other number. */ pathOf(fd: number): string; /** * Open a file, the open's effect landing before any byte moves * (`MountCore.open`), and answer the core's handle for it. `perm` is the * mode bits a create gives the file; null takes the mount's default. */ open(path: string, mode: OpenMode, perm?: number | null): Promise<number>; /** * Read through an open handle: what it holds, judged as a read of its * path now, with what it wrote and has not landed laid over. */ pread(fd: number, offset: number, size: number): Promise<Uint8Array>; /** * Write through an open handle, which keeps the bytes until a close or a * sync lands them; one that appends writes at its end. */ pwrite(fd: number, offset: number, data: Uint8Array): Promise<void>; /** The row an open handle stats by, the size it holds counted. */ fstat(fd: number): Promise<VFSStat>; /** Whether the name an open handle's file had went, so the file has no link left. */ unlinked(fd: number): boolean; /** Set the length of the file an open handle is on. */ ftruncate(fd: number, length: number): Promise<void>; /** Land what an open handle wrote, as fsync(2) does. */ fsync(fd: number): Promise<void>; /** * Land what an open handle wrote and let it go; a landing that fails * still lets it go, and is thrown. */ close(fd: number): Promise<void>; /** * The path an op on `path` lands on once the namespace's links are * followed: the final name too when `follow`, else only the directories * above it, since a rename or a removal acts on the name itself. One file * reads as one path whichever spelling reached it, which is how a removal * finds the files open on it. Mirrors Python's RuntimeFiles.walked. */ walked(path: string, follow?: boolean): string; /** * The workspace mount prefixes, longest first, trailing-slash * normalized. Longest first is what makes mountOf's first match the * right one when one mount nests inside another. */ prefixes(): string[]; /** * The mount prefix serving `path`, longest match first, or null. The * resolver answers in the mount table's own spelling; this surface * re-spells to its trailing-slash convention, the form `prefixes` * reports. */ mountOf(path: string): string | null; /** * Whether the workspace answers for `path`. * * A guest's content calls gate on this: monty and quickjs refuse a * path the workspace does not serve, so they read and write only the * view. A mount serves what is under it, and a namespace link serves * what is reached through it wherever it lives, because the * dispatcher follows a link outside every mount the same way. With no * mounts wired there is no scoping, and every path routes here. */ serves(path: string): boolean; /** * A file's bytes, or the range of them a handle asked for. `raw` reads * the stored bytes rather than a rendering, which is what an edit that is * written back must start from; `direct` reads what the backend holds * now, past the file cache. Mirrors Python's `RuntimeFiles.read`. */ read(path: string, options?: { offset?: number; size?: number; raw?: boolean; direct?: boolean; }): Promise<Uint8Array>; write(path: string, bytes: Uint8Array): Promise<void>; /** * One path's metadata, projected for a guest encoder. * * @param path guest-absolute virtual path. * @param nofollow report a trailing symlink itself rather than its * target (a guest's lstat). The row is then the node table's own, * so it carries the target string's length as the size, the link's * mtime, and whatever a `chown -h` wrote; the dispatcher consumes * the flag and gates that read exactly as it gates `readlink`. */ stat(path: string, nofollow?: boolean): Promise<VFSStat>; private mountRow; /** * The path's row, or null when the mount says it is not there. Only an * absence (`ABSENT_PATH`) answers null; anything else throws, since a * refusal is not an answer about the path. */ statOrNull(path: string, nofollow?: boolean): Promise<VFSStat | null>; /** * `path`'s row as a runtime may see it, or null. * * Structure is open and content is not. A path in the view (`serves`) * answers with its mount's own row; any path the workspace lists * answers as a directory, so the root above nested mounts and the * directories above a link are directories here as they are in a * shell, while a withheld surface's files (history, the program view) * stay unseen. A file's own row decides that, not its listing: the * history mount lists its one file as empty so a traversal never * descends into it. 0 is the file adapter's spelling of an unknown mtime. */ viewStat(path: string): Promise<VFSStat | null>; /** * The directory's unclassified rows, or null when it is not one: the * question a guest asks of a path with no row of its own, a directory * a mount only implies (the root above a nested mount), so nothing * per entry is stat'd. Asked in the directory form, so a prefix store * never answers `/s3/f` with the keys of `/s3/foo.txt`. */ listingOrNull(path: string): Promise<VFSEntry[] | null>; /** * List a directory as resolved entries (Python's `readdir` shape). * * A backend that slash-marks directories skips the stat; every other * entry is classified by its own stat, which is RAM when the readdir * filled the index and a backend request when the mount keeps none. * At most `LISTING_ENTRY_CONCURRENCY` requests run at once across * everything this adapter serves, so a large directory on an unindexed * mount does not put every entry's request on the wire together. * * An entry whose stat fails, for any reason, rides unclassified: a * size-0 non-directory with no mode and no stamp, the row that says * "not known". One entry never fails the listing, the way a kernel * readdir never stats at all. What went wrong is not lost: the * guest's own stat or open of that entry asks the mount again and * reports it. Only the listing itself failing fails the call. Any * failure but a missing path (a dangling link, an entry gone since the * listing) also warns on the host, since the row the guest sees is * degraded. * * A row that did stat carries its mode and stamp too, since the * struct is already in hand: a guest that places a directory's nodes * from one listing (Emscripten does) then needs no second stat per * file. * The slash-marked and unclassified rows report neither, which is * the honest answer for a listing that never learned them. * * The link mark comes from the name plane, since no backend listing * reports a link. One table read per listing, and it only ever marks * a name the listing itself returned, so a link the session hides * stays hidden: the dispatcher filtered it out of the entries above * and an unmatched mark marks nothing. A marked row is the link's * own, as a guest's lstat reads it, since the node table answers that * stat and no backend is asked. * * @param path guest-absolute virtual path of the directory. * @param classify stat each entry to learn its kind. A guest that * only needs names (monty's listdir, quickjs) passes false and * every row comes back unclassified, one request for the listing * and none per entry, as a POSIX readdir costs. */ readdir(path: string, classify?: boolean): Promise<VFSEntry[]>; private classified; /** Set `path`'s length: a shrink drops bytes, growth reads zeros. */ truncate(path: string, length?: number): Promise<void>; /** * Remove the entry at `path`; a file open on it keeps what it held, as * POSIX keeps an open file on its inode. */ unlink(path: string): Promise<void>; /** * Create a directory; `parents` asks the mount to create missing * ancestors too (pathlib's mkdir(parents=True), which the backend op * takes as a flag on both hosts). */ mkdir(path: string, parents?: boolean): Promise<void>; rmdir(path: string): Promise<void>; /** * Rename within one mount. * * Args: * src: guest-absolute source path. * dst: guest-absolute destination path. * * Throws: * EXDEV: the two ends resolve to different mounts. */ rename(src: string, dst: string): Promise<void>; /** * Create a namespace symlink at `path` pointing at `target`. * * A link is namespace state, so no backend stores one and the target * is kept verbatim as the guest typed it. The dispatcher answers this * op from the node table itself, which is why a runtime can serve * `os.symlink` at all: the dispatcher a surface already holds reaches the * name plane, not just a mount. * * Args: * path: guest-absolute path of the link to create. * target: link target, stored as typed. */ symlink(path: string, target: string): Promise<void>; /** * The target of the symlink at `path`. * * Throws EINVAL when `path` is not a link, which is what the node * table answers and what POSIX readlink says. */ readlink(path: string): Promise<string>; /** * Write metadata fields, natively where the backend can hold them. * * The dispatcher reads the whole set and stores in the namespace overlay * whatever the backend cannot keep, so a mount with no setattr op * still answers: a utime on an s3 or dropbox mount lands in the name * plane and stat reports it back. Stored, not enforced; the mount * mode is the access control. * * Args: * path: guest-absolute virtual path. * attrs: the fields to write, unset ones omitted. */ setattr(path: string, attrs: SetAttrFields): Promise<void>; /** * Extend `path` by `tail`, falling back to a whole-file write. * * `append` is optional per backend (S3 registers `write` and * `rename` without it), so a mount that declines is remembered: the * fallback then costs one failed dispatch per mount rather than one * per call. * * The fallback reads the base fresh every time, never a copy from an * earlier append: an append lands after whatever the file holds now, * so a write another action made between two appends is kept, as * O_APPEND keeps it. It reads the stored bytes, not a rendering, since * it writes them back. Only a confirmed absence starts from an empty * base, since an append may create the file; every other read failure * propagates, because writing the tail alone over a file that exists * but is momentarily unreadable would replace content this run never * saw. * * Args: * path: guest-absolute virtual path. * tail: only the newly appended bytes. */ append(path: string, tail: Uint8Array): Promise<void>; private appendDelta; } //# sourceMappingURL=files.d.ts.map