/** * Layer-2 placeholder rehydration on the write path: an Edit `new_string` or a * Write `content` carrying `[hidden HTML removed #]` / `[HTML comment * removed #]` placeholders (copied from sanitized tool output) has each * one restored to the stored original bytes from the reveal store's span files. * * SECURITY invariant: the stored span content was REDACTED before persistence * (sanitize-output runs strict web-ingress redaction on each splice original * before persistSpan), so this rehydration can never write a raw secret — the * worst it can restore is `[REDACTED…]` placeholder text standing where the * secret was, which the on-disk tripwire then flags on later Reads. * * Composition with the secret rehydrator: this runs AFTER it (a later terminal * layer), and the two grammars are disjoint — a Layer-2 placeholder never * matches the `[REDACTED…]` grammar and vice versa — so neither can touch the * other's tokens. Running second also means a restored original that contains * `[REDACTED…]` text is never re-fed to the secret resolver (which would deny * it as a foreign placeholder). * * `old_string` is deliberately NOT rehydrated: a Layer-2 placeholder there * exists in the model's view of PRIOR TOOL OUTPUT, not on disk, so unless the * file literally contains the placeholder text, Edit's ordinary no-match * failure is the right outcome — no re-anchoring. MultiEdit/NotebookEdit with a * Layer-2 placeholder are denied (parity with the secret path: sequential * edits / notebook JSON cannot be rehydrated). * @param {string} tool * @param {any} toolInput * @returns {{ updatedInput: any, context: string } | { deny: string } | null} */ export function rehydrateLayer2(tool: string, toolInput: any): { updatedInput: any; context: string; } | { deny: string; } | null; /** * The declared layer chain, layers 2-4. Every entry states the two properties * the driver reasons about — whether it ERASES code points another layer reads, * and whether its own decision is SKIP-BASED and therefore invalidated by such * an erasure — so the confusable fold's ordering precondition is enforced by the * table instead of restated as a comment. * * Layer 3 is dropped from the table (not merely skipped at run time) when * AGENT_SANITIZER_OUTPUT_DISABLED=1, so the driver sees the chain that will * actually run: with no erasing layer left after the fold there is no fixed * point to reach and no extra pass to pay for. * @param {(tool: string, toolInput: any) => ReturnType} rehydrate * @param {NodeJS.ProcessEnv | Record} [env] * @returns {import("./lib/layer-pipeline.mjs").Layer[]} */ export function preToolUseLayers(rehydrate: (tool: string, toolInput: any) => ReturnType, env?: NodeJS.ProcessEnv | Record): import("./lib/layer-pipeline.mjs").Layer[]; /** * Compose the four protections. Returns the `hookSpecificOutput` fields to * emit, or null for a clean no-op. Throws only if a layer's engine throws; the * caller fails closed (ask) on any throw. Every exit routes through emitTraced. * @param {any} input parsed PreToolUse event * @param {(tool: string, toolInput: any) => ReturnType} [rehydrate] * injectable for tests; the default binds the real redactor-daemon io (the * layer reads the target file and maps secrets through the daemon) * @param {import("./lib/trace.mjs").TraceFn} [sink] where engagement is * announced; a host with its own trace channel passes its sink (see lib/trace.mjs) * @returns {Promise | null>} */ export function buildPreToolUseResponse(input: any, rehydrate?: (tool: string, toolInput: any) => ReturnType, sink?: import("./lib/trace.mjs").TraceFn): Promise | null>; /** * Agent-agnostic judge over the four protections: consumes a control-plane * ToolCallEvent and returns a Verdict, so a non-Claude host can run the same * sanitization pipeline through its own adapter. The wired Claude CLI below * routes through this judge and renders the Verdict with the Claude adapter; on * any throw (a control-plane package-load failure included) it falls back to * failClosedFields — a native response that needs no package — so the * fail-closed posture holds even when the adapter never loaded. * @param {import("agent-control-plane-core").ToolCallEvent} event * @param {(tool: string, toolInput: any) => ReturnType} [rehydrate] * @param {{ * messages?: Partial, * gates?: HostGate[], * trace?: import("./lib/trace.mjs").TraceFn, * }} [opts] * messages are merged over the defaults, so a partial table is supported * @returns {Promise} */ export function judgePreToolUseSanitize(event: import("agent-control-plane-core").ToolCallEvent, rehydrate?: (tool: string, toolInput: any) => ReturnType, opts?: { messages?: Partial; gates?: HostGate[]; trace?: import("./lib/trace.mjs").TraceFn; }): Promise; /** * The dependency-load failure hiding behind a hook error, or "". A binding that * never loaded surfaces at use time as a bare TypeError ("X is not a function") * that names neither the package nor the remedy; when any lazily-loaded package * has a recorded load error, name it — the failed set is derived from the * loader's own records, so a future dependency is covered without editing a list * here. An error already reporting a missing package (missingPackageError's * `DEP_UNAVAILABLE` tag) gets no second copy. * @param {unknown} err * @param {string} [remedy] what a reader should run; hosts pass their own * @param {() => string[]} [failedPackages] * @param {(pkg: string) => unknown} [loadErrorFor] * @returns {string} */ export function depLoadHint(err: unknown, remedy?: string, failedPackages?: () => string[], loadErrorFor?: (pkg: string) => unknown): string; /** * The fail-closed hookSpecificOutput fields for a hook-level failure, chosen by * WHICH failure it was. Corrupt/unparsable INPUT (`parsedOk` false — a JSON parse * error or the oversize-body cap) is a state an adversary can induce with no * upside to failing, so it hard-DENIES: no human to talk past, no approval * fatigue, no latency. A LAYER/engine throw after a clean parse (`parsedOk` true * — redactor daemon down, package not loaded) is the sanitizer being UNAVAILABLE, * so it ASKS to keep a human in the loop rather than hard-block on infrastructure. * * An UNATTENDED host has no human for that ask to reach, so the ask stops the * call and buys no review. Such a host passes `unavailableDecision: DENY` and * gets a hard refusal on the clean-parse arm too, with the same reason text. * The default stays ASK, so a host that wires nothing keeps today's behavior. * * Deliberately knob-blind: this is the fail-CLOSED posture itself, so it ignores * AGENT_SANITIZER_FAIL_OPEN — a host that wires it directly keeps strictness by * construction, with no env var to remember. A host that instead wants the * caller's posture (fail-open by default) wires {@link hookFailureFields}, * which delegates here when the posture is closed. * @param {boolean} parsedOk whether the input parsed before the failure * @param {unknown} err * @param {{ * messages?: Partial, * hint?: string, * unavailableDecision?: (typeof PermissionDecision)[keyof typeof PermissionDecision], * }} [opts] * @returns {Record} */ export function failClosedFields(parsedOk: boolean, err: unknown, opts?: { messages?: Partial; hint?: string; unavailableDecision?: (typeof PermissionDecision)[keyof typeof PermissionDecision]; }): Record; /** * True when a faulting PreToolUse call is the one case the OPEN posture must * still not pass: a write-shaped tool whose input carries the * redaction-placeholder prefix. Such a placeholder stands for a secret the * sanitizer redacted out of the model's view; with the sanitizer down, * rehydration cannot translate it back, so letting the call through would * persist the literal placeholder text over the real secret on disk — a * destructive clobber, not a missed scan. Package-free by construction (a Set * lookup and a substring test on the already-parsed payload), so it holds when * the failure IS the missing package. * @param {unknown} input raw parsed PreToolUse payload (undefined if unparsed) * @returns {boolean} */ export function hintedWriteFault(input: unknown): boolean; /** * The hookSpecificOutput fields for a hook-level failure under the CALLER's * chosen posture: fail-OPEN by default — a warning context and no * permissionDecision, so the tool call proceeds unsanitized — or the * fail-CLOSED verdict of {@link failClosedFields} when the caller set * AGENT_SANITIZER_FAIL_OPEN=0. The open default has ONE carve-out, declared in * this hook's fault policy below: a write-shaped call whose input carries a * `[REDACTED…]` placeholder asks instead of passing (see * {@link hintedWriteFault}) — pass `input` so the policy can see it. * * The posture covers this hook's own failures, whatever their cause. What it * does NOT cover is the verdict of a sanitizer that ran: a payload * judgePreToolUseSanitize denied is denied in both postures. * @param {boolean} parsedOk whether the input parsed before the failure * @param {unknown} err * @param {{ * messages?: Partial, * hint?: string, * env?: NodeJS.ProcessEnv | Record, * input?: unknown, * }} [opts] * @returns {Record} */ export function hookFailureFields(parsedOk: boolean, err: unknown, opts?: { messages?: Partial; hint?: string; env?: NodeJS.ProcessEnv | Record; input?: unknown; }): Record; /** * The hook's CLI: parse → judge → render, under the caller's failure posture. * Exported so a bundle entry (which must claim the CLI slot before this module * loads) can run the exact same wiring instead of duplicating the onError * posture. * @param {{ * messages?: Partial, * gates?: HostGate[], * trace?: import("./lib/trace.mjs").TraceFn, * }} [opts] * @returns {Promise} */ export function cliMain(opts?: { messages?: Partial; gates?: HostGate[]; trace?: import("./lib/trace.mjs").TraceFn; }): Promise; /** * A host-supplied deny gate: given the PreToolUse input, the reason this call * must be blocked, or null to let the pipeline continue. Hosts use these for * policy the package has no view of (a required workflow step, a project-local * rule); the package ships none. * @typedef {(input: { tool_name: string | null, tool_input: any, session_id?: string, * permission_mode?: string }) * => string | null | undefined} HostGate */ /** * The reasons this hook emits, as a table a host overrides. A host that knows * which of ITS files wires the adapter, and what a reader should do about a * failure, can say so — the package cannot, since it has no idea where it is * installed. * @type {Readonly<{ * unknownEvent: string, * failed: (cause: string) => string, * unparsable: (cause: string) => string, * remedy: string, * }>} */ export const PRE_TOOL_USE_MESSAGES: Readonly<{ unknownEvent: string; failed: (cause: string) => string; unparsable: (cause: string) => string; remedy: string; }>; export const REDACTION_HINT: "[REDACTED"; export const WRITE_SHAPED_TOOLS: Set; /** * A host-supplied deny gate: given the PreToolUse input, the reason this call * must be blocked, or null to let the pipeline continue. Hosts use these for * policy the package has no view of (a required workflow step, a project-local * rule); the package ships none. */ export type HostGate = (input: { tool_name: string | null; tool_input: any; session_id?: string; permission_mode?: string; }) => string | null | undefined; declare const rehydrateRedacted: typeof import("agent-sanitizer/rehydrate").rehydrateRedacted; import { PermissionDecision } from "./lib/hook-io.mjs"; export {};