/** * Path confinement for values that came from an analyzed repository. * * OpenLore reads repositories it does not trust (SECURITY.md scopes exactly that), * and two of the values it acts on are attacker-authored: `openspecPath` in the * committed `.openlore/config.json`, and the layout of `openspec/` itself — a repo * can commit a SYMLINK there, and git will check it out. Neither may be allowed to * redirect a read or a write outside the project root. * * These primitives live in a leaf module (no OpenLore imports) because the sites * that need them span the MCP handlers, the CLI, the decision syncer and the * analyzer; a shared guard that only one face can import is how the drift that * motivated this file happened in the first place. */ import { type Stats } from 'node:fs'; /** * The canonical (symlink-resolved) path of `p`, or — when `p` does not exist (a * write target) — the canonical path of its nearest existing ancestor. Used to * confine on the REAL filesystem location rather than the lexical path. */ export declare function realPathOrNearestExisting(p: string): string; /** * Resolve a user-supplied relative file path against a validated project root and * ensure the result stays within that root — by BOTH a lexical check (cheap, blocks * `../` traversal) AND a canonical, symlink-resolved check (mcp-security: * Symlink-Aware Path Confinement). The canonical check defeats an in-root symlink * that points outside the root: confinement is enforced on the real path of the * target where it exists, and on the real path of its nearest existing ancestor * where it does not (so a not-yet-created write target is confined too). */ export declare function safeJoin(absDir: string, filePath: string): string; /** * Read a repository file through one descriptor and disclose its contents only while * that descriptor still names the canonically confined file. The repeated identity * check closes the `safeJoin` -> `readFile` swap window for artifact-derived paths: * replacing the file or one of its parent directories makes the read fail closed. */ export declare function readFileConfined(absDir: string, filePath: string, maxBytes?: number, rejectSymlinkPath?: boolean, fatalUtf8?: boolean): Promise; export interface ConfinedFileRead { content: string; /** Metadata captured from the same open descriptor after the read completed. */ stat: Stats; } /** * The freshness-aware form of {@link readFileConfined}. Content and metadata come * from one descriptor, and neither is returned if that file or its name changes * during the read. */ export declare function readFileConfinedWithStat(absDir: string, filePath: string, maxBytes?: number, rejectSymlinkPath?: boolean, fatalUtf8?: boolean): Promise; /** * True when `absPath` stays inside `absRoot` both lexically and canonically. * * The predicate form of {@link safeJoin}, for the walkers that enumerate paths * themselves (a `readdir` of `openspec/specs`) rather than joining a caller's * string: they need to DROP an escaping entry and carry on, not abort the walk. */ export declare function isConfinedPath(absRoot: string, absPath: string): boolean; /** * Recover an interrupted expected-identity publication. Callers must serialize * this target with the same advisory lock used for the corresponding write. */ export declare function recoverConfinedAtomicWriteFile(absRoot: string, absPath: string, recoveryJournalPath: string): Promise; /** Atomically replace a repository file without following any repository symlink. */ export declare function confinedAtomicWriteFile(absRoot: string, absPath: string, data: string, options?: { mode?: number; preserveMode?: boolean; /** Publish only if the target still has this identity; null means it must remain absent. */ expectedIdentity?: Pick | null; /** Exact bytes read with expectedIdentity, for the decisive post-rename comparison. */ expectedContent?: string; /** Trusted, lock-bound journal used to recover an interrupted guarded publication. */ recoveryJournalPath?: string; }): Promise; /** * Resolve the project's openspec directory, confined to the validated root. * * `config.openspecPath` is read from `.openlore/config.json` — an untrusted on-disk * artifact (mcp-security threat model). A poisoned value (`../../etc`, an absolute * escape) must not redirect the reads/writes that derive from it (spec/manifest * reads, decision ADR reads, decision sync writes) outside the project root. We * confine via safeJoin; a value that escapes the root falls back to the default * `openspec/` dir — a legitimate in-root path (default or custom) passes through * unchanged, so only an escaping value is neutralized. */ export declare function safeOpenspecDir(absRoot: string, configuredPath: string | undefined, onFallback?: (message: string) => void): string; //# sourceMappingURL=path-confinement.d.ts.map