/** * The bounded, untrusted-artifact read every `.openlore/` reader in this repo is required to use. * * Extracted from `mcp-handlers/artifact-cache.ts` (change: refine-first-run-partial-serving) so * the analyzer, the runtime and the serving layer can all share ONE implementation. It moved * because a second reader needed it and reached across a layer boundary to get it — and a second * COPY of it is exactly how one of them ends up without the FIFO check. * * These files are written by other processes and, in the case a hostile repository ships them, * by an adversary. Reads therefore follow the repo's untrusted-artifact rules: no symlink * following, regular files only, a byte ceiling enforced on the READ rather than on a prior * stat, and an identity check taken from the same descriptor that produced the bytes. */ import { type FileHandle } from 'node:fs/promises'; /** * Identity stamp of an on-disk artifact, or `null` when it is absent or unreadable. * * `mtimeNs` (not `mtimeMs`) is what makes the stamp usable when a file is rewritten * twice inside the same millisecond; `dev`/`ino` catch an atomic tmp-file rename that * lands carrying an older mtime; `ctimeNs` catches a same-size in-place rewrite that * an mtime-only stamp would miss. The same fields `vector-index.ts` stamps with. * * The resolution is the filesystem's, not ours: where timestamps are coarser than the * interval between two writes (NTFS is the common case), two same-size rewrites inside * one tick are indistinguishable and the cached value is served until the next change. * Every writer of these artifacts goes through the atomic tmp-file-and-rename path, * which changes `ino`, so this is a bound on a case the repo does not produce rather * than on ordinary operation. */ export declare function artifactStamp(path: string): Promise; /** * Default ceiling: for the SIBLING artifacts (parse health, style fingerprint, and the like). * * These are repository-controlled input. Real ones are single-digit megabytes; the cap exists so * a poisoned or runaway file fails closed instead of being parsed and then RETAINED for the * process lifetime, which is what a cache makes newly dangerous. Deliberately lower than * {@link ANALYSIS_ARTIFACT_MAX_BYTES}, which callers pass explicitly for the graph-sized * artifacts — a reader that needs the larger ceiling asks for it. */ export declare const MAX_ARTIFACT_BYTES: number; /** * Ceiling for the analysis artifacts themselves. * * Higher than {@link MAX_ARTIFACT_BYTES} because `llm-context.json` on a large repository is * legitimately hundreds of megabytes — this matches the ceiling the context reader already * applies. The point of bounding these at all is to fail closed on a poisoned file rather than * OOM, not to cap what a real analysis is allowed to produce. */ export declare const ANALYSIS_ARTIFACT_MAX_BYTES: number; /** One artifact read: its text and the stamp of the bytes actually read, or null. */ export interface StampedArtifact { text: string; stamp: string; } export declare function descriptorIsThePathEntry(handle: FileHandle, path: string): Promise; /** * Read an artifact through a single descriptor, bounded, with the stamp taken from * that same descriptor. * * Three properties the obvious `stat(path)` + `readFile(path)` form does not have: * * Pass `republishedConcurrently` for a file its own writer rewrites continuously (the * analysis progress sidecar). The identity checks exist so the returned bytes and the * returned stamp describe the same entry, which is the right contract for a write-once * artifact — but a hot file is republished by write-temp-then-rename, so those checks fail * as a matter of course and a caller reading "refused" as "not there" reports no analysis * running while one is (measured: 234 false absences in 400 reads). The option relaxes ONLY * the inode-identity comparison. The explicit symlink refusal, O_NONBLOCK and the * isFile/size ceiling still apply, so a symlink, a FIFO and an oversized file are refused * on every platform; the stamp is then taken from the opened descriptor, which is the entry * actually read. * * - **The ceiling bounds the READ.** A prior stat only describes the file at that * instant; a file that grows afterwards is still read to EOF. Reading in chunks up * to `MAX_ARTIFACT_BYTES + 1` fails closed instead. * - **The stamp describes the bytes returned.** A stamp re-taken from the PATH after * the read is worse than no stamp at all: a writer landing in that window makes the * cache store the old content under the new file's stamp, so every later call * serves stale content believing it current. An `fstat` on the open descriptor, * plus a pre/post identity check, cannot mis-attribute that way. * - **No symlink, no special file.** A symlink committed into `.openlore/analysis/` * must not redirect the read, and a FIFO must not stall the handler. `O_NOFOLLOW` * is the race-free form and is what POSIX honours, but libuv does NOT implement it * on Windows: there the flag is silently ignored and the link is followed. So the * open is also VERIFIED afterwards — the descriptor's identity is compared against * the path entry's, and a path that is a link (or resolves to a different inode * than the descriptor holds) is refused. Verifying after the open rather than * checking before it is what makes this sound: the bytes come from the descriptor, * so an entry swapped after the open cannot redirect the read, and an entry that * was already a link is caught. The `isFile` check on the descriptor rejects FIFOs * and directories. */ /** * Why a bounded read produced nothing. * * `absent` and `refused` are deliberately distinguished: a caller that treats "no such file" as * a legitimate state (an analysis predating manifests, say) must not treat "this file is a FIFO, * a symlink, or too large" the same way. Collapsing them is how a poisoned artifact gets read as * a missing one and quietly downgraded. */ export type BoundedReadResult = { state: 'ok'; bytes: Buffer; stamp: string; } | { state: 'absent'; } | { state: 'refused'; }; /** The bytes of an artifact, read under the discipline described above. */ export declare function readArtifactBytesBounded(path: string, maxBytes?: number, options?: { republishedConcurrently?: boolean; }): Promise; export declare function readArtifactBounded(path: string, maxBytes?: number): Promise; //# sourceMappingURL=bounded-artifact-read.d.ts.map