/** * The warning prose for a value the caller asked for and is not getting, * because the redactor could not vet it for secrets. * * Exported so the hook layer composes the same sentence for the artifacts it * withholds itself (the reveal sidecar) rather than restating the wording. * @param {string} label what was withheld, as a noun phrase * @returns {string} */ export function withheldWarning(label: string): string; /** * Delete each verbatim span in `spans` from `text`. The secure Layer-5 * primitive: a filter can only ask for deletions, so this can never inject * bytes. Returns the new text and how many span occurrences were removed (0 * when no span was present). * * Every occurrence is located in the ORIGINAL `text` and the deletions applied * in one ordered pass ({@link spliceOrdered}), so every removed byte lies inside * a match some span had in the INPUT. Deleting span-by-span with a * chained `split`/`join` would not hold that line: an earlier deletion joins the * bytes on either side of it and can CREATE a match for a later span that never * occurred in the input — `deleteVerbatimSpans("PRE-XX-POST", ["-XX-", "PREPOST"])` * then deletes the whole document. That would widen the Layer-5 seam's blast * radius (see the module doc) from "a compromised filter can at most remove the * content it named" to "it can remove content it never named". * * Overlapping spans are resolved first-match-wins, so `removed` counts the * occurrences actually spliced out, never a double-count of the same bytes. * @param {string} text * @param {string[]} spans * @returns {{ text: string, removed: number }} */ export function deleteVerbatimSpans(text: string, spans: string[]): { text: string; removed: number; }; /** * @typedef {{ * html?: boolean, * exfilScan?: boolean, * flagDigestValues?: boolean, * redact?: (text: string) => Promise | (RedactResult|null), * filterInjection?: (text: string) => Promise | (Layer5Result|null), * sgrCarveOut?: boolean, * deadline?: Deadline, * }} SanitizeTextOptions */ /** * A caller's shared wall-clock budget across one run of this pipeline. * `remainingMs()` returns the milliseconds left; at or below zero it is spent. * Layer 4's injected redactor reads its own copy of the same budget, so this * option is what lets the layers inside this module read it too. Omitted means * no budget, which is the standalone default: every layer runs to completion. * @typedef {{ remainingMs: () => number }} Deadline */ /** * Run the configured layers over a single text blob. Layer 1 always runs; the * rest are opt-in via `options`. Layer 4 (`redact`) is the fail-closed path: a * redactor that throws is rethrown wrapped, so the caller suppresses the * output rather than emitting an unvetted value. That fail-closed behavior * also applies to Layer 4's re-scan after a Layer-5 span deletion (see Layer * 5, below) — a redactor failure there fails the whole call closed too. * `reveal` is the pre-Layer-2 text, present only when the HTML splice removed * bytes, so a caller can persist what was hidden for later inspection (see * {@link applyMarkdownPipeline}); the field is omitted otherwise, and also when * it could not be vetted (see {@link vetStageValue}). `splices` is its * per-placeholder twin — Layer 2's placeholder→original pairs, in document * order, so a hook can rehydrate individual splices (the keyed-placeholder * grammar lives in ./html.mjs: `layer2Placeholder`/`LAYER2_PLACEHOLDER_RE`). * Present only when Layer 2 spliced; each `original` is vetted like `reveal`, * and one that cannot be vetted is WITHHELD (dropped from the array) under the * same doctrine — Layer 4 never saw pre-splice text. * * Every byte mutation goes through {@link applyMutation} and every Layer-4 call * through {@link runRedact}, so a layer cannot re-establish some of the * post-mutation invariants and forget the rest, and every string in the returned * object has traversed Layer 4. * * Findings come back SPLIT BY SEVERITY (see ./severity.mjs): `warnings` holds * everything injection-shaped — the banner a caller must show — and `notes` * holds what happened but is not alarming. `warnings` therefore keeps exactly * the meaning it always had, and a caller that ignores `notes` is no louder * than before, just quieter about incidental bytes. * * `found` is the machine-readable twin, and the severity split does NOT reach * it — the {@link CATEGORY} codes for what Layers 1-3 neutralized or flagged, * in the order the layers ran, whichever tier described them. Layers 4 and 5 * have no category codes (their findings are the injected seam's own * vocabulary), so they contribute findings only. * @param {string} text * @param {SanitizeTextOptions} [options] * @returns {Promise<{ cleaned: string, found: string[], warnings: string[], notes: string[], modified: boolean, sgrNote: boolean, reveal?: string, splices?: Array<{ placeholder: string, original: string }> }>} */ export function sanitizeText(text: string, options?: SanitizeTextOptions): Promise<{ cleaned: string; found: string[]; warnings: string[]; notes: string[]; modified: boolean; sgrNote: boolean; reveal?: string; splices?: Array<{ placeholder: string; original: string; }>; }>; /** * True only for arrays and PLAIN objects — the two shapes whose contents are * safe to walk via `Object.entries` without silently dropping data. An exotic * object (Map/Set/Date/RegExp/typed array/class instance) carries its data in * internal slots that `Object.entries` does not enumerate, so descending into * one and rebuilding it from its entries corrupts it to `{}` (or an empty * clone). Those pass through as OPAQUE LEAVES instead — unchanged — preserving * the tool-output shape a harness matches on. A null-prototype object is treated * as plain (its own enumerable string keys are the whole story). * @param {any} value * @returns {boolean} */ export function isWalkableContainer(value: any): boolean; /** * Sanitize every string leaf of a tool-output value, preserving its shape (a * structured tool output whose shape changes would be ignored by a harness, * leaking the raw value). Non-string leaves pass through; `warnings` and * `notes` accumulate across leaves, split by severity (see ./severity.mjs). * `sgrNote` is the OR across leaves — true when SOME leaf was note-only — so a * caller can still pick the quiet banner when no leaf raised a warning. * * Fails CLOSED on two hostile shapes that would otherwise throw a `RangeError` * as an unhandled async rejection (a DoS that leaves the output un-sanitized): * nesting past {@link MAX_DEPTH}, and a reference cycle. Either replaces the * offending subtree with a placeholder string + a warning, never passing the * raw subtree through. Keys are also screened for hidden chars (see below). * * `reveals` accumulates each string leaf's pre-Layer-2 text (present only when * the HTML splice removed bytes) so a caller can persist what was hidden — the * structured-output analogue of {@link sanitizeText}'s `reveal`. Same * mutated-accumulator contract as `warnings`. * * `splices` accumulates each string leaf's Layer-2 placeholder→original pairs * (each `original` already vetted, withheld entries dropped — see * {@link sanitizeText}) — the per-placeholder twin of `reveals`, so a hook * caller gets them for object-shaped tool output too. Same mutated-accumulator * contract as `reveals`. * @param {any} value * @param {SanitizeTextOptions} options * @param {string[]} warnings * @param {string[]} [reveals] * @param {string[]} [notes] the NOTE-severity counterpart of `warnings`; * appended last so an existing positional caller keeps working (it simply * discards the notes, which is exactly as loud as before the split) * @param {Array<{ placeholder: string, original: string }>} [splices] appended * after `notes` for the same positional-compatibility reason * @returns {Promise<{ value: any, modified: boolean, sgrNote: boolean }>} */ export function sanitizeValue(value: any, options: SanitizeTextOptions, warnings: string[], reveals?: string[], notes?: string[], splices?: Array<{ placeholder: string; original: string; }>): Promise<{ value: any; modified: boolean; sgrNote: boolean; }>; /** * Compose the model-facing context line for a sanitized/flagged tool output. * `injectionAlert` is the caller's optional trailing alert (e.g. appended only * for untrusted-ingress tools where a semantic-injection filter actually ran). * @param {boolean} modified output bytes were changed (vs. flagged only) * @param {string[]} warnings * @param {{ injectionAlert?: string }} [options] * @returns {string} */ export function composeContext(modified: boolean, warnings: string[], { injectionAlert }?: { injectionAlert?: string; }): string; /** * Replace every string leaf of `value` with `message`, preserving shape so a * fail-closed placeholder matches the tool's output schema. Non-string leaves * pass through. An Anthropic content block is the one exception: it is * collapsed whole to `{ type: "text", text: message }`, because rewriting its * `type` tag produces a block the API rejects (see {@link isContentBlock}). * * Shares {@link sanitizeValue}'s depth/cycle guard for the same reason: this * runs on the fail-closed path (an already-suspect output), so a 200k-deep or * self-referential value must NOT blow the stack here — that would re-open the * very hole suppression exists to close. Past {@link MAX_DEPTH} or on a cycle it * substitutes `message` for the offending subtree (already the suppression * sentinel, so the placeholder is consistent with the rest of the output). A * recognised block collapses BEFORE either guard and recurses no further, so it * is subject to neither; a truncated subtree that is not itself a block is * still replaced by the bare string, block position or not. * @param {any} value * @param {string} message * @returns {any} */ export function suppressToolOutput(value: any, message: string): any; /** * Closed enum of LIBRARY-OWNED Layer-5 warning codes — the ONLY warning values * the injected `filterInjection` seam may return. This mirrors the `found`-code * contract (`CATEGORY` in ./invisible.mjs): the seam speaks a fixed vocabulary * of codes, and the LIBRARY owns the human-readable string each maps to. Free * text from the filter is REFUSED (see `mapFilterWarning`), because the filter * runs on attacker-influenced content and its output is concatenated into the * model-facing context WITHOUT passing back through Layer 1 — so a compromised * or prompt-injected filter that could emit arbitrary `warning` text would * defeat the "a compromised filter can only remove bytes, never inject" seam * contract. Branch on these codes; the prose below is not part of the contract. * @type {Readonly<{ SPANS_REMOVED: "spans-removed", FILTER_FLAGGED: "filter-flagged", FILTER_ERROR: "filter-error" }>} */ export const FILTER_WARNING: Readonly<{ SPANS_REMOVED: "spans-removed"; FILTER_FLAGGED: "filter-flagged"; FILTER_ERROR: "filter-error"; }>; /** * The doctrine clause riding every redaction warning — the one moment * placeholders enter the model's view. Without it the model has no way to know * that a placeholder written back through any path but Edit/Write (a heredoc, * sed/tee, an MCP file tool) is persisted literally, destroying the secret — * making that route-around an honest mistake rather than a warned one. * Exported so tests assert the composed warning by reference instead of * re-typing the prose. */ export const REDACTION_DOCTRINE: string; export { needsMarkdownPipeline }; /** * Maximum container nesting `sanitizeValue` / `suppressToolOutput` will descend * before failing closed. The JS engine's own call-stack limit is many thousands * of frames deep, so 200 is a wide safety margin below it: a real tool output * never nests this far, while a hostile 200k-deep array (or a self-referential * cycle) would otherwise blow the stack as an UNHANDLED async rejection — the * output then escapes sanitization entirely (fail-open DoS). Past this depth the * subtree is replaced with a placeholder and a warning is recorded, so the * caller still emits a sanitized, flagged result instead of crashing. */ export const MAX_DEPTH: 200; /** * Layer-4 result: the redacted text, the category labels redacted, and an * optional caller-supplied annotation appended to the warning. */ export type RedactResult = { text: string; found: string[]; note?: string; }; /** * A {@link FILTER_WARNING} enum code — the closed vocabulary the Layer-5 seam * may return in `warning`. See FILTER_WARNING for the meanings. */ export type FilterWarningCode = "spans-removed" | "filter-flagged" | "filter-error"; /** * Layer-5 result: verbatim spans to delete (the only mutation a filter may * request) and/or a warning CODE (never free text — the library owns the * message). Null means the filter made no finding. */ export type Layer5Result = { removeSpans?: string[]; warning?: FilterWarningCode; }; /** * The running state of one {@link sanitizeText} call. Layers read `text` and * mutate it ONLY through {@link applyMutation}. Findings carry their own * severity (see ./severity.mjs) and are split into `warnings`/`notes` at the * single exit, so no layer can push into the wrong list. `found` is the * machine-readable twin, unaffected by the split: the {@link CATEGORY} codes * for whatever Layers 1-3 neutralized or flagged. */ export type PipelineState = { text: string; found: string[]; findings: import("./severity.mjs").Finding[]; modified: boolean; unreportedChange: boolean; }; export type SanitizeTextOptions = { html?: boolean; exfilScan?: boolean; flagDigestValues?: boolean; redact?: (text: string) => Promise | (RedactResult | null); filterInjection?: (text: string) => Promise | (Layer5Result | null); sgrCarveOut?: boolean; deadline?: Deadline; }; /** * A caller's shared wall-clock budget across one run of this pipeline. * `remainingMs()` returns the milliseconds left; at or below zero it is spent. * Layer 4's injected redactor reads its own copy of the same budget, so this * option is what lets the layers inside this module read it too. Omitted means * no budget, which is the standalone default: every layer runs to completion. */ export type Deadline = { remainingMs: () => number; }; /** * a content-block field's value test */ export type FieldShape = (v: any) => boolean; export type BlockSchema = { required: Record; optional: Record; }; import { needsMarkdownPipeline } from "./gates.mjs"; export { describeExfil, describeRemoved, describeWarned } from "./warnings.mjs";