UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

524 lines • 25.9 kB
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