@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
524 lines • 25.9 kB
TypeScript
import { type Files } from '../workspace/files.ts';
import { FileTable, type OpenMode } from '../runtime/handles/index.ts';
import { FileStat } from '../types.ts';
import type { SessionState } from '../workspace/session/session.ts';
import type { MountAttrs, Handle } from './types.ts';
export interface MountCoreOptions {
rootPrefix?: string;
/**
* Bind every op to this session's mount grants. The kernel-tier
* primitive: bind-mount the tree into a container and the narrowing
* travels with it. Enforcement happens inside dispatch/Files via the
* session context, so binding at the op entry point is sufficient.
*/
session?: SessionState;
/**
* Who owns what the mount shows when a row names its owner by name (the
* kernel wants numbers): the mounting user, which a kernel adapter reads
* off its process. A guest owns nothing, so it defaults to 0.
*/
owner?: {
uid: number;
gid: number;
};
}
/**
* Protocol-neutral mount logic shared by every kernel adapter.
*
* Everything here is expressed in POSIX terms (attribute records, ordinary
* thrown errors) and imports nothing from `@zkochan/fuse-native`, so it is
* reusable by a non-FUSE adapter and unit-testable without a kernel.
*
* The division of labour: this class decides *what* the filesystem
* contains, an adapter decides *how* to say it to a particular kernel
* interface. Adapters translate the errors thrown here into their own
* error codes with `classifyErrno`. Mirrors Python's `MountCore`.
*
* Every op goes through `ws.vfs`, which delegates to the dispatcher, so
* a mount walks the same dispatcher as a shell line (mount modes, policies,
* cache, invalidation) and every op it runs lands on the `ws.vfs.records`
* ledger. Reaching `ws.dispatch` from here instead would skip the record;
* reaching a backend directly would skip the dispatcher.
*/
export declare class MountCore {
readonly files: Files;
readonly session: SessionState | null;
private readonly now;
private readonly root;
readonly handles: FileTable<Handle>;
private static readonly grown;
private readonly hydrations;
private readonly hydrationGen;
private readonly pending;
private readonly removals;
private readonly opening;
private readonly uid;
private readonly gid;
private readonly noAppend;
private removed;
constructor(files: Files, options?: MountCoreOptions);
/**
* Run one op under the bound session's mount grants and profile, as the
* Python MountCore binds each op: dispatch reads the session from
* context, so binding at the op entry point is what scopes a kernel or
* SFTP mount to it. Unbound, an op runs as whatever session is current.
*/
private op;
/** Judge retained metadata through the stat policy boundary. */
private statCached;
/** Judge retained bytes through the same read boundary as a fetch. */
private readCached;
resolve(path: string): string;
/**
* The attrs for one stat row, the way a guest's stat reads it. The row
* carries the namespace overlay (chmod bits, chown ids, a touched mtime),
* so what a metadata op stored is what the mount shows, and a device
* keeps its type and numbers. String uid/gid (names) fall back to the
* mounting user: the kernel wants numbers and there is no user db to map
* against. A missing stamp falls back to the mount's start time; epoch
* zero is a real time and lands. `size` replaces the row's, from an open
* handle or a link's shown target. Mirrors Python's `MountCore.attrs`.
*/
attrs(s: FileStat, size?: number | null): MountAttrs;
/**
* The target to present for a link at a mount path. Relative targets are
* stored verbatim and returned as-is. Absolute targets name virtual
* paths, so they are rewritten relative to the link's directory: returned
* raw, the kernel would resolve them against the host root and escape
* the mountpoint.
*/
shownTarget(path: string, target: string): string;
/**
* The length of the bytes an open handle on the file holds. A size-unknown
* file is read whole when it opens, so while a handle is open its length
* answers a stat by path too (`ls -l` beside a `cat`). Once every handle
* is released, the dispatcher answers from the workspace cache instead.
* Mirrors Python's `held_size`.
*/
heldSize(path: string): number | null;
/**
* Read a size-unknown file whole for the handle opening it, through the
* dispatcher, so a caching mount keeps the bytes for the next open and for
* a stat once this one closes. Undefined when the read fails: open() stays
* permissive and the read() that follows surfaces the error. Opens of one
* view of one file while a read is out share it; `raw` is the stored
* bytes, for a handle that writes, rather than the rendering. Mirrors
* Python's `_hydrate`.
*/
private hydrate;
/**
* Run one mutation of a file after every mutation already queued for
* it, and let the next one wait for it. Serialization is per identity,
* so a flush through a link and a truncate through its target queue
* behind each other, and a flush still landing cannot be overtaken by a
* truncate that would then be undone when the flush completes.
*/
private mutate;
/**
* Run `fn` holding the chain of every key, taken in sorted order so two
* callers that hold the same pair never wait on each other. Mirrors
* Python's `_queue_all`.
*/
private queueAll;
/**
* Run `fn`, which removes, replaces or moves the files at `paths`, after
* the opens of those names already out, with later ones held back until
* it is done, as the kernel orders an open and an unlink or a rename of
* one name. `hold` reads the rest for the handles open before; an open
* that slipped in while that read was out would get a chunked handle onto
* bytes about to go. Chains of their own, taken before any `pending` one,
* rather than `pending` itself: an open they wait for may be truncating
* there.
*/
private removing;
/**
* Run `fn` after every call already queued under `key` in `queues`, and
* let the next one wait for it, whether it resolves or throws.
*/
private queue;
/**
* Land write runs on the mount, one pwrite each, in order. A pwrite keeps
* every stored byte the handle did not write, so nothing is read through
* the dispatcher first: a session that may write a file and not read it writes
* through FUSE, as through a write-only descriptor. An appending handle's
* runs land as appends instead, at the end the file has then, so what
* another writer appended meanwhile stays before them, as O_APPEND keeps
* it. The runs that landed leave `runs` in one step, so after a failure
* `runs` holds only what did not land and a retry never replays a run over
* bytes another writer has since put there. A run that fails still
* invalidates what the core holds, since the runs before it have landed.
*/
private applyWrites;
/**
* Extend a file by `data` at the end it has now. A mount with no append
* (S3) takes a write of the whole file, read fresh from the backend
* rather than from a copy, so a write another action made since stays; a
* mount that declined once is remembered. Mirrors Python's `_append`.
*/
private appendTo;
/** The read options for one view: the stored bytes, else the rendering. */
private static stored;
/** The key one view of a file's hydration goes by. */
private static view;
/**
* POSIX attributes for a path. One stat through the dispatcher answers: a
* link the session cannot see is absent, as it is to the shell, and a
* visible one reports its own row with its target read through the
* dispatcher too. `follow` reports a trailing link's target rather than
* the link (stat rather than lstat).
*/
getattr(path: string, follow?: boolean, ctx?: Handle | null): Promise<MountAttrs>;
/**
* The row a stat of `path` answers, the size an open handle holds and what
* it wrote and has not landed counted. Mirrors Python's `_row`.
*/
private row;
/**
* Attributes through an open handle: the path's row, with the size the
* handle holds, what it wrote and has not flushed included.
*/
fgetattr(path: string, fd: number): Promise<MountAttrs>;
/**
* The row a stat of `path` answers, what the handles open on it wrote and
* have not landed counted, as a guest projects it into its own record.
* Mirrors Python's `stat`.
*/
stat(path: string, follow?: boolean): Promise<FileStat>;
/**
* The row an open handle stats by: its file's, with the size it holds and
* what it wrote and has not landed counted, judged as a stat of its path.
* A guest projects this into its own record. Mirrors Python's `fstat`.
*/
fstat(fd: number): Promise<FileStat>;
/** The handle `fd` names; EBADF for any other number. */
private openHandle;
/**
* Where the file a handle holds ends: its bytes, else the length its open
* saw, and past them what it wrote.
*/
private static end;
/**
* The mount root's attrs: its own row through the dispatcher, so a chmod
* made on it shows, or a plain directory when nothing answers for it (a
* workspace with no mount at `/`). Mirrors Python's `root_attrs`.
*/
rootAttrs(): Promise<MountAttrs>;
readdir(path: string): Promise<string[]>;
/**
* A directory's entries with their attributes, in one pass. The entries
* are stat'd together rather than one after another, with at most
* `LISTING_CONCURRENCY` stats out at once across every listing. A stat
* that fails ends the listing, and the ones not yet sent are not. An
* entry that vanishes between the listing and its stat is left out, as
* `ls` leaves out a file deleted mid-listing. `names` are the entries to
* stat, in the order they are answered; null takes `readdir`'s, `.` and
* `..` first. Mirrors Python's `listing`.
*/
listing(path: string, names?: readonly string[] | null, follow?: boolean): Promise<[string, MountAttrs][]>;
read(path: string, fd: number, pos: number, len: number): Promise<Uint8Array>;
/**
* What a read of the file `key` names sees that has not landed: every
* handle's writes on it, as write(2) is seen by every reader at once.
* `supersede` keeps them apart, so they lay over the stored bytes in any
* order; an appending handle's are its own, since they land at the end the
* file has then, and the reader's own go last. Mirrors Python's
* `_writes_on`.
*/
private writesOn;
/**
* Buffer a write on its handle, or apply it directly when there is none.
* An appending handle writes at the end it holds, whatever `pos` says;
* EBADF through a handle opened only to read.
*/
write(path: string, fd: number, data: Uint8Array, pos: number): Promise<void>;
/**
* Keep a write on its handle, a copy: one that starts inside the last run
* or right at its end folds into it, so a stream of writes stays one run;
* any other starts a run of its own, laid over the earlier ones in order,
* and every 64 of those fold together while none waits in the spool. Past
* `MEMORY_LIMIT` held in memory, the handle's runs move to its spool, and
* a stream goes on at the spool's end. A spool append is queued as it is
* called, ahead of any read of the window it publishes. Mirrors Python's
* `_buffer`.
*/
private static buffer;
/** The buffered writes held in memory, in arrival order. */
private static held;
/**
* Move a handle's runs held in memory to its spool once they hold more
* than `MEMORY_LIMIT`. Each takes its place in the spool before the spool
* has its bytes, in one append, so a write or a read meanwhile finds the
* run where it will be. Mirrors Python's `_spill`.
*/
private static spill;
/**
* `run` with `data` laid at `at`, inside it or right at its end: in place
* when the run is one this grew and its buffer has the room, else in a
* buffer twice as long, so a stream copies what it holds only as often as
* its length doubles, as Python's bytearray grows.
*/
private static folded;
/**
* Cut a new write's range out of what the file's other handles hold:
* their bytes there are older, and the new write replaces them. No two
* handles on a file then hold the same bytes, so whichever flush, release
* or sync lands first, the file ends as its writes were made. Nothing
* lands here, so a write never waits on one.
*/
private supersede;
/** `writes` with the range a newer write took cut out of them. Mirrors Python's `_cut`. */
private static cut;
/**
* Put a failed landing's writes back ahead of those made since, less what
* other writes took meanwhile, so a retry never lands bytes older than
* theirs. Mirrors Python's `_put_back`.
*/
private static putBack;
/**
* Create an empty file and return a fresh handle. `mode` is what the
* creator asked for, umask applied; null takes the mount's default.
*/
create(path: string, mode?: number | null): Promise<number>;
/** Create an empty file in the file's turn. */
private make;
/** Create a directory, keeping the mode it was asked for. */
mkdir(path: string, mode?: number | null): Promise<void>;
/**
* Store the mode a create asked for, when it is not the one the backend
* made the entry with (a disk mount applies the daemon's umask), so
* `open(O_CREAT, 0600)` and `mkdir -m` read back as asked without a
* write per ordinary create. Mirrors Python's `_keep_mode`.
*/
private keepMode;
/** The target of a namespace link, read through the dispatcher; EINVAL when not a link. */
readlink(path: string): Promise<string>;
/**
* Create namespace link `dest -> src` (ln -s src dest; libfuse passes
* the pointee first). Relative sources are stored verbatim (resolved
* at follow time, exactly like the shell `ln -s`); absolute sources
* are mapped into virtual space so a scoped mount stores the path it
* will later follow.
*/
symlink(src: string, dest: string): Promise<void>;
/**
* Remove the entry at `path`, a link entry like any other.
*
* A link routes through the dispatcher rather than straight to the node
* table: `unlink` is a LINK_ENTRY_OPS member, so the dispatcher answers a
* link path itself, gated by session grants and admission policies
* and recorded on the ledger. Writing the table here instead let a
* session-scoped kernel mount delete a link on a mount its profile
* hides. Mirrors Python's MountCore.unlink.
*/
unlink(path: string): Promise<void>;
/**
* The one function every mutation of a file's bytes goes through. The
* open handles on a file are matched by its identity (the mount path with
* namespace links followed), and this is the only place their bytes are
* refreshed, so a new mutating op cannot forget one of them and a link
* alias cannot slip past. A hydration still in flight is outdated so it
* re-reads instead of handing over what it fetched, and hydrated handles
* on the file are refreshed in one read through the dispatcher, so fstat
* and read through any of them, including the handle that wrote, see the
* new bytes. A removal or rename passes `rehydrate = false`: POSIX keeps
* an open descriptor on the bytes it had, and the name, not what it
* points at, is what changed. A refresh that fails is logged
* and leaves the handles unhydrated rather than failing the committed
* mutation. Mirrors Python's `_changed`.
*/
private changed;
/**
* Resize the file an open handle is on: through its name, or, on a file
* whose name went, the bytes its handles hold. EBADF through a handle
* opened only to read, EINVAL for a negative length. Mirrors Python's
* `ftruncate`.
*/
ftruncate(fd: number, length: number): Promise<void>;
rename(src: string, dst: string): Promise<void>;
rmdir(path: string): Promise<void>;
/**
* The mount path the file `key` names, or `spelled` where it lies outside
* the mount's root.
*/
private mountPath;
/**
* Where a mount path really points: the mount-resolved path with every
* namespace link followed, so two handles opened through a link and
* its target are recognised as the same file.
*/
identity(path: string, follow?: boolean): string;
/**
* Persist a handle's buffered writes. The buffer is detached before the
* await so a write arriving meanwhile is not lost to the clear, and the
* runs that did not land are restored ahead of those later writes when
* persistence fails, so the acknowledged bytes stay for the handle's own
* flush to retry.
*/
private settle;
/** The handles open on the entry at `path` itself. */
private named;
/**
* The handles open on the file `key` names. The handles left on a file
* whose name went share a key of their own (`detach`), so no path names
* them.
*/
private openOn;
/**
* Leave the handles open on a name that went on the file it had: they
* stat by its row from then on, and share a key no path gives, so a write
* through one reaches the others and nothing that opens the name later.
*/
private detach;
/**
* Land a handle's writes on a file whose name went: on the bytes it holds,
* which every handle open on that file reads, as writes to an unlinked
* file stay with it. A handle reading a rendering the stored bytes differ
* from keeps its own. The writes leave the handle before its spool is read,
* an empty run at their end in their place, so a write arriving meanwhile
* stays for the next landing and an append starts past them; the spool
* stays the handle's, closed once none of its writes wait there. Mirrors
* Python's `_land_held`.
*/
private landHeld;
/**
* Land what every handle open on `path` wrote, in the file's turn, as a
* change of its times by path must come after; `follow` says the change
* follows a link at `path`. Mirrors Python's `land`.
*/
land(path: string, follow?: boolean): Promise<void>;
/**
* Have the handles open on `path` stat it afresh, after a change by path
* made past the core (a runtime's setattr or whole-file write), so their
* next fstat agrees with stat; `follow` says the change followed a link
* at `path`. Mirrors Python's `restat`.
*/
restat(path: string, follow?: boolean): void;
/**
* Drop the stat rows the handles open on `key` keep, and outdate a stat
* of theirs still out. Mirrors Python's `_restat`.
*/
private restatKey;
/** Land the buffered writes of every handle open on `key`. */
private landBuffered;
private persistBuffered;
/**
* Land a buffer whose writes wait in its spool, each group of writes
* `writeRuns` would fold into one run as a stream, in order. A group from
* the file's start that reaches its stored end leaves nothing of what is
* stored, so it lands as a write of the whole file, which a store with no
* pwrite (S3) takes in parts rather than reading the file back first; any
* other lands as one streamed pwrite, and an appending handle's as
* appends. The groups that landed leave `groups`, as `applyWrites` drops
* its runs. Mirrors Python's `_land_spooled`.
*/
private landSpooled;
/**
* Whether the stored file at `path` is missing or ends at or before `end`,
* by a fresh stat, which waits out a write already landing on the path, so
* an append under way is counted. A stat refused (a policy may allow the
* write and not it) answers no, so the group lands as a pwrite rather than
* not at all. Mirrors Python's `_ends_by`.
*/
private endsBy;
/** Close a handle's spool once none of its writes wait there. Mirrors Python's `_free_spool`. */
private static freeSpool;
truncate(path: string, size: number): Promise<void>;
statfs(): Record<string, number>;
/**
* Store metadata through the dispatcher. The backend keeps what it can and
* the namespace overlay the rest, so a chmod, chown or `touch -d` through
* the mount is what `stat` in a shell reads back, on a backend with no
* permission bits or settable times of its own too. A null field is left
* as it is. Mirrors Python's `MountCore.setattr`.
*/
setattr(path: string, mode: number | null, uid?: number | null, gid?: number | null, atime?: Date | null, mtime?: Date | null, follow?: boolean): Promise<void>;
/**
* Store an extended attribute through the dispatcher, which keeps
* it on the path's namespace node: it outlives the mount, moves with a
* rename, and is the attribute every other surface (the shell's
* getfattr, a guest's os.getxattr) reads. Tools that set xattrs as a
* matter of course (rsync -aX, tar --xattrs, cp -p, Finder writing
* com.apple.*) succeed on a backend with no attribute slot of its own.
* Mirrors the python MountCore.
*/
setxattr(path: string, name: string, value: Uint8Array, opts?: {
create?: boolean;
replace?: boolean;
}): Promise<void>;
/** One attribute, the backend's own facts included; ENODATA when unset. */
getxattr(path: string, name: string): Promise<Uint8Array>;
listxattr(path: string): Promise<string[]>;
removexattr(path: string, name: string): Promise<void>;
/**
* Open a path, the open's effect landing before any byte moves. One rule
* for every entry point, however it spells the open (open(2) flags, SFTP
* pflags, an fopen mode, preview1 oflags): an exclusive create refuses
* what exists, a dangling link or a listed directory included; a
* directory refuses a mode that would write it; a missing path is created
* when the mode creates and refused when it does not; a truncating mode
* empties what exists, which CPython's `open('w')` leaves behind even when
* nothing is written; and a mode that writes is admitted against the
* file's write rules before its first write. A file an API mount cannot
* size is then read whole. `perm` is the mode bits a create gives the
* file, umask applied; null takes the mount's default. Mirrors Python's
* `MountCore.open`.
*/
open(path: string, mode?: OpenMode, perm?: number | null): Promise<number>;
/** The open itself, which a removal of its name waits for. */
private openPath;
/** The path's row, or null when the mount says it is not there. */
private rowOrNull;
/**
* Whether a path with no row is a directory the mount only implies (the
* root above a nested mount), which it lists.
*/
private listed;
/**
* Empty a file an open truncates. A mount with no truncate (hf buckets,
* databricks volumes) still empties it through an empty create, which is
* the effect the open asked for.
*/
private empty;
/**
* A removal of `path` still running. One is keyed by the name it takes, so
* an open through a link meets the link's removal and then its target's.
*/
private removalOf;
/**
* Keep what the handles open on `path` need once it goes, and return the
* row they stat by from then on. POSIX keeps an open descriptor on the
* file it had, so an unlink or a rename onto it must not leave a handle
* reading or stat'ing the file at that name next. A handle holding no
* bytes yet (never read, or chunked and holding one chunk) gets them all
* from one read every such handle shares; it runs under `removing` and
* the file's `pending` chain, so no handle opens on the file and no write
* lands on it meanwhile. The handles are matched by the
* entry itself, so removing a link holds nothing: it takes the link,
* never its target's bytes. A stat or a read that fails (a policy may
* allow the removal and refuse either) leaves a bare row or the handles
* as they are, each on its own, rather than refusing a mutation the
* caller is allowed. Mirrors Python's `MountCore._hold`.
*/
private hold;
/**
* Wait for a flush or truncation of the handle's file still landing, into
* the queue a rename moved the handle to. Mirrors Python's `_settled`.
*/
private settled;
/**
* Land a handle's buffered writes and let it go. The handle goes even
* when the landing fails, as close(2) releases its descriptor: no
* adapter retries a release, so a kept handle would never close. The
* failure still reaches the caller.
*/
release(fd: number): Promise<void>;
flush(_path: string, fd: number): Promise<void>;
/**
* Land what every open handle holds, as sync(2) does, for a client whose
* close(2) returns before it releases the handle (sshfs). A landing that
* fails is logged, and its bytes stay on the handle for its own flush to
* retry and report.
*/
sync(): Promise<void>;
}
//# sourceMappingURL=core.d.ts.map