import type { ExecutionEnv } from "../../internal/harness.js"; /** * batch-B (CC 2.1.187 parity): Read/Edit/Write take the file path as `file_path`, with the legacy `path` as a * durable back-compat alias. Read the target the same way EVERYWHERE the path is consumed from a tool-call's args — * not just the tool body but ALSO every gate that extracts the write target to confine it (sensitive-path, * skill allowPaths, session allowDirs). A gate that only read `args.path` would see `undefined` for a `file_path` * call and either fail-OPEN (bypass the guard — a real hole) or fail-closed (deny a legitimate write). Single source. */ export declare function fileArgPath(args: unknown): string | undefined; /** * The "hand" file tools (design/44) — safety invariants ported from CC's `FileEditTool` (design/43 Rule * 7): without these a weak model blind-edits / overwrites files (P0 safety, not polish). This module is * the pure/testable core (path resolution + the three edit invariants + content hashing); the tools in * this dir compose it. Per-task state and the execution env are passed in (factory-closure wiring, * design/44 §11 ruling A), never read from a shared/global. */ /** What we remember about a file the agent has read (design/44 §4). */ export interface ReadEntry { /** SHA-256 of the content at read time. Staleness is detected by content hash, NOT mtime — mtime is * unreliable across containers/volumes/NFS and across a design/45 suspend→resume on another replica. */ hash: string; /** Total line count at read time, and whether the read was truncated (for the edit truncation hint, S2). */ totalLines: number; truncated: boolean; /** The line window actually served to the model on the last read (design/64 §7.2(6) dedup stub). Lets * read_file return a `file_unchanged` stub when the SAME window is re-requested and the content hash is * unchanged — saving the re-transmitted body. Optional/additive: undefined (e.g. an entry seeded from an * older design/45 checkpoint) just means "no dedup", never a false hit. */ view?: { start: number; end: number; }; /** Millisecond timestamp of the last read/write-back that recorded this entry (blackboard * 2026-07-03, compact re-read parity: the post-compaction attachment picks the most RECENTLY * read files — CC sorts readFileState by timestamp). Optional/additive: undefined (an entry * seeded from an older checkpoint) sorts last, never breaks. */ lastReadAt?: number; /** CC 2.1.204 parity (`seededFromContext`, cc204-bundle @17889409 / seeding @17917159; 198 zero * hits): TRUE when the Runner pre-seeded this entry because the file's FULL disk-verbatim text * was injected into the model's context at startup (ProjectMemoryLoad.seededFiles — CC's * CLAUDE.md/nested-memory seeding). A DEFAULT whole-file Read of an UNCHANGED seeded file * answers with the already-in-context reminder instead of re-transmitting the body. Any real * Read / edit write-back records a FRESH entry without this flag, so a file that changed on * disk (hash mismatch) always serves real content and the dedup disarms permanently. * Optional/additive: absent = normal entry. */ seededFromContext?: boolean; } /** Per-task record of which files have been read, keyed by canonical path (design/44 §4, §11 ruling A: * owned by prepare-task, closure-captured by the tools; serialized into a design/45 Checkpoint later). */ export type ReadFileState = Map; /** SHA-256 hex of UTF-8 content — the staleness/identity key (design/44 §4 inv 2, jury M1). */ export declare function sha256(content: string): string; /** A failed path resolution / invariant check, surfaced to the model as a self-correctable tool error. */ export interface FsViolation { code: "path_not_in_root" | "not_read" | "stale" | "ambiguous_edit" | "invalid"; message: string; } export declare function isBlockedDevicePath(key: string): boolean; /** True if `path`'s extension is a known binary format (pure check — no I/O). Used by read_file and grep. */ export declare function hasBinaryExtension(path: string): boolean; /** The MIME type to return an image file AS (a visual `ImageContent` block), or `undefined` if `path` is not a * Read-supported image. Pure — no I/O. */ export declare function imageMimeForRead(path: string): string | undefined; /** Verify the raw bytes actually ARE the image format the extension claims (magic-number sniff). Extension alone * is forgeable: a `.png` holding text / an empty file / a truncated blob would otherwise become a malformed * `ImageContent` that makes the NEXT provider request fail (400). Pure — no I/O. (batch-C, codex review.) */ export declare function imageMagicMatches(bytes: Uint8Array, mimeType: string): boolean; /** * Content sniff for binary data (design/64 §17.1, CC `constants/files.ts` `isBinaryContent`): a NUL byte, * or more than 10% non-printable characters in the sample, means binary. Second layer behind * {@link hasBinaryExtension} — catches extension-less or mis-named binaries (a `Dockerfile` that is really * a compiled blob) that would otherwise be read as mojibake. Plain text (incl. UTF-8, tabs/newlines) passes. */ export declare function isBinaryContent(sample: string): boolean; /** True for a Windows/SMB UNC path (`\\host\share`) or a `//`-prefixed network path — refused to avoid * NTLM credential leaks (design/64 §7.2(8), CC validateInput). Pure check on the RAW model path. */ export declare function isUncPath(path: string): boolean; /** * Resolve a model-supplied path to a canonical key AND enforce rootPath containment (design/44 §4 * inv 5/6/7). Existing paths use `canonicalPath` (resolves symlinks, so the same file can't get two * keys). New paths canonicalize the **deepest existing ancestor** then rejoin the missing tail — a * symlinked parent therefore can't smuggle the target outside root. Returns the canonical key, or a * `path_not_in_root` violation. (Defense-in-depth at the tool layer; NOT a substitute for a sandboxed * `executionEnv` — see design/44 §5.) */ export declare function resolveKey(env: ExecutionEnv, rootCanonical: string, path: string, signal?: AbortSignal, baseCwd?: string, additionalRootsCanonical?: readonly string[]): Promise<{ ok: true; key: string; } | { ok: false; violation: FsViolation; }>; /** * Canonicalize a model-supplied path to its real on-disk target (symlinks resolved; for a path that * doesn't exist yet, the deepest EXISTING ancestor is canonicalized then the missing tail rejoined — * so a symlinked parent can't smuggle the target elsewhere). NO containment check — this is the pure * canonicalization shared by `resolveKey` (which adds rootPath containment) and * `createSensitivePathPolicy` (design/72 §2.1, which matches the real target against a guarded list). * A single source of truth for symlink resolution: a sensitive-path guard that matched the raw path * instead of the real target would be one-shot bypassable by a symlink (the bug §2.1 calls out). */ export declare function canonicalizeTarget(env: ExecutionEnv, path: string, signal?: AbortSignal, baseCwd?: string): Promise<{ ok: true; key: string; } | { ok: false; message: string; unresolvedSymlink?: true; }>; /** Render a `FsViolation` as the model-facing tool error text (returned, never thrown — model retries). */ export declare function violationText(toolName: string, v: FsViolation): string; /** inv 1 (read-before-edit): a file must have been read this task before it can be edited/overwritten. * Message is CC 2.1.198 live-verbatim (all-tools-live-probe 2026-07-08 §2.1/§3.1/§5.1 — one message for * Edit/Write/NotebookEdit: "before writing to it", not the old sema "before editing"). */ export declare function requireRead(state: ReadFileState, key: string): FsViolation | undefined; /** * Edit "no-op" guard (design/64 §7.2(4), CC `FileEditTool:148-153`): refuse an edit whose `old_string` * equals `new_string` — without it the edit passes the match check and writes the file back unchanged (a * silent no-op that wastes a turn and dirties mtime). Verbatim CC message so a CC-trained model recognizes it. */ export declare function checkNoChange(oldString: string, newString: string): FsViolation | undefined; /** inv 2 (staleness): the file's current content hash must match what was recorded at read time. */ export declare function checkStale(entry: ReadEntry, currentHash: string): FsViolation | undefined; /** Count non-overlapping occurrences of `needle` in `haystack` (`needle` must be non-empty). */ export declare function countOccurrences(haystack: string, needle: string): number; /** * Suggest a sibling filename for a missing path (CC `File does not exist… Did you mean X?` self-heal * path, bundle :335883/:478419). Honest scope (1.253 双轨终审): this is the FALLBACK tier — an * approximation of CC's Cxe same-directory scan plus a sema-added case-insensitive exact-name match * (the case-typo class CC misses). CC's FIRST preference (aY: re-resolve the filename against cwd * and suggest the cwd path on an exists hit) needs env probing and therefore lives at the call site * (fs/index.ts enoentMessage) — this function stays pure over a listed sibling set. (a) CC's * findSimilarFile rule — same stem, different extension/name in the same directory; (b) the * case-insensitive exact-name match. Returns the suggested NAME (not a full path), or undefined. */ export declare function similarNameSuggestion(siblingNames: readonly string[], missingName: string): string | undefined; /** Replace curly single/double quotes with their straight ASCII forms (CC qDa, 1:1 char mapping). */ export declare function normalizeQuotes(s: string): string; /** * Resolve `oldString` against `content` with the CC quote-forgiveness layer: exact match first; on a * miss, match in the curly→straight normalized coordinate and return the ACTUAL file substring at * that span (CC kOe). Returns undefined when even the normalized form does not appear. */ export declare function resolveQuoteMatch(content: string, oldString: string): string | undefined; /** * When the quote-forgiving match resolved to a curly-quoted file span, convert the straight quotes in * `newString` to the matching curly forms so the replacement stays style-consistent with the file * (CC Dmt/yPp/TPp: openers vs closers by preceding char; a letter-adjacent single quote is an * apostrophe U+2019). Call ONLY when the matched span differs from the model's old_string. */ export declare function adaptNewStringQuotes(matchedOld: string, newString: string): string; /** * The effective old_string a DELETION should replace: `oldString + "\n"` when new_string is empty, * old_string doesn't already end with a newline, and the newline-suffixed form exists in the content; * otherwise old_string unchanged (CC KDa parity). SINGLE (non-replace_all) edits only: a widened * needle under replace_all would miss occurrences not followed by a newline and desync the * replacement count — the Edit tool restricts the widening to single edits (1.253 双轨终审 MED). */ export declare function deletionOldString(content: string, oldString: string, newString: string): string; /** * inv 3 (edit uniqueness): `oldString` must match exactly once unless `replaceAll`. Zero → not found; * >1 without replaceAll → ambiguous. `truncated` adds a hint that unseen content may hold more matches (S2). * Messages are CC 2.1.198 live-verbatim (all-tools-live-probe 2026-07-08 §2.1): first sentence(s) byte-exact, * then a `String: ` echo line (CC form). The sema truncated-read hint survives as a parenthetical * appended to the sentence (probe verdict: "truncated 提示可并入括注保留"). */ export declare function checkEditMatch(content: string, oldString: string, replaceAll: boolean, truncated: boolean): FsViolation | undefined; //# sourceMappingURL=safety.d.ts.map