@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
396 lines • 19.1 kB
TypeScript
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