import { type Stats } from "node:fs"; /** * Run a synchronous fs operation, retrying ONLY the transient Windows lock codes * in {@link TRANSIENT_LOCK_CODES} with a short bounded backoff (~0.5s worst case). * Any other error — `EEXIST` from an exclusive create, a genuine `EACCES` on a * locked-down path that never clears — is re-thrown on its first occurrence, so * this absorbs the sub-millisecond scanner window without ever masking a real * failure. The retry preserves the caller's atomicity/rollback guarantees: it * re-issues the same single syscall, nothing more. * * Exported for direct unit testing — the FS-level retry is exercised through the * real filesystem elsewhere, but a transient lock cannot be reproduced on demand, * so the retry/give-up/passthrough contract is pinned here. */ export declare function retryTransient(op: () => T): T; interface StagedWrite { path: string; contents: string; mode?: number; expect?: { absent: true; } | { sha256: string; }; } export interface AppliedWrite { path: string; contents: string; backup?: string; created: boolean; } interface AppliedRemoval { path: string; legacyPath: string; } export interface FsTxnResult { written: string[]; backups: string[]; /** Files moved out of the tree (source → `.aih/legacy/` destination). */ removed: AppliedRemoval[]; } /** * Stages writes in memory and commits them atomically. Each existing target is * first copied to `.aih.bak`; new content is written to a temp file and * `rename`d into place (atomic on the same volume). If a later write throws, * targets that still hold this transaction's generated bytes are rolled back; * a concurrently changed target is preserved along with its backup. */ export declare class FsTransaction { private staged; private stagedRemovals; private stagedAssertions; stage(path: string, contents: string, mode?: number, expect?: { absent: true; } | { sha256: string; }): void; /** * Stage a file REMOVAL as a reversible move to `legacyPath` (under gitignored * `.aih/legacy/`). The move IS the backup: rollback (and the user) restore by * moving it back. Symlinks are refused at commit (moving a link then restoring it * would recreate a regular file). No-op if the source is already gone. * `backupSibling` marks a hard-delete destination (`.aih.bak`): still * never-overwrite, but a taken slot falls back to `.N.aih.bak` (matches the * gitignored `*.aih.bak` glob) instead of the archive's `.N`. */ stageRemoval(path: string, legacyPath: string, opts?: { backupSibling?: boolean; expect?: { sha256: string; }; }): void; stageAssertion(path: string, sha256: string, describe: string): void; preview(): ReadonlyArray; commit(): FsTxnResult; } /** * Restore only targets that still contain this transaction's generated bytes. * A later operator edit is never overwritten during best-effort rollback; its * pre-transaction backup remains beside the target for recovery. */ /** * Exported for direct unit testing: a real concurrent write is not schedulable * deterministically between this synchronous transaction's filesystem calls. */ export declare function rollbackAppliedWrites(applied: AppliedWrite[]): string[]; /** Read a file's text, or `undefined` if it does not exist. */ export declare function readIfExists(path: string): string | undefined; /** * Open-then-read on ONE file descriptor: the regular-file check (`fstat` on the * open fd, never a second path lookup) and the read cannot be raced apart. * Where the platform exposes `O_NOFOLLOW`, a symlink swapped in after directory * enumeration is refused at open rather than silently followed. Where it does * not, the fallback verifies after opening that the path still names the same * non-symlink regular file as the descriptor before reading. Returns undefined * for anything that is not a readable regular file. * * Use this — not {@link readIfExists} — for any path DISCOVERED by a directory * scan: a plain exists-then-read pair on a scanned path is a swap window where * a symlink planted between enumeration and read gets silently followed and its * target's bytes laundered into an artifact (marketplace build, evidence * bundle, fleet bundle all package what they read). */ export declare function readRegularFileWithStats(abs: string, options?: { maxBytes?: number; }): { contents: Buffer; stats: Stats; } | undefined; /** * Read at most `maxBytes + 1` bytes from an already-open descriptor. The extra * byte distinguishes an exact-boundary file from one that grew after an earlier * `fstat`; returning `undefined` keeps the caller's byte cap effective during * the read instead of only before it. */ export declare function readBoundedFileDescriptor(fd: number, maxBytes: number): Buffer | undefined; export declare function readRegularFile(abs: string, options?: { maxBytes?: number; }): Buffer | undefined; export {};