import { type AssistantMessage, type JsonSchema } from "./anthropic.js"; import { type ToolUseValidator } from "./validator.js"; import { type Reshaper } from "./reshaper.js"; export type RepairOutcome = "fixed" | "failed" | "refused" | "refused_destructive" | "cancelled"; export interface RepairDecision { outcome: RepairOutcome; message?: AssistantMessage; } export interface RepairDeps { validator: ToolUseValidator; reshaper: Reshaper; maxAttempts: number; isDestructive: (toolName: string) => boolean; /** * The model that actually SERVED the failing response, reported to the reshaper * so it can see which backend produced the malformed call. Optional because the * caller owns target resolution; absent ⇒ null (unknown), never a guess. */ backendModel?: string | null; /** Cancellation of the caller response; never use it to start another repair. */ signal?: AbortSignal; } /** * Attempt to repair an invalid tool-bearing assistant message. Safety-first: * if ANY tool_use in the (invalid) response targets a destructive tool, we * REFUSE rather than fabricate its arguments — because repair output may run * under --dangerously-skip-permissions. Otherwise reshape ≤ maxAttempts, * re-validating each attempt; a reshape that doesn't validate is never emitted. * * Every reshaper answer additionally passes `guardReshaped()` before it can be * emitted, so the safety verdict is taken on what we are about to RETURN, not * only on what the backend sent. */ export declare function repair(assistant: AssistantMessage, tools: Map, deps: RepairDeps): Promise; /** * Repair double-encoded tool inputs across a message, returning the corrected message or null * when nothing provable changed. Structure is conserved by construction — same blocks, same * order, same tool_use ids and names, only `input` values decoded — so this needs no * `guardReshaped`: it is not a collaborator, it is arithmetic on the schema. */ export declare function decodeDoubleEncodedInputs(message: AssistantMessage, tools: Map): AssistantMessage | null; /** * The post-reshape safety gate, taken on the message we are about to emit. * * Two checks, both PURE FORM — no reading of what the arguments mean: * * 1. **Destructive names.** The pre-check only sees what the BACKEND sent. A * `Reshaper` is an interface, so `repair()` cannot assume the in-tree * `reconstruct()` (which happens to map only `input`) is the implementation * on the other side of the call. The boundary function must check what it * returns, not trust a collaborator's internals. * 2. **Structural conservation.** The reshaper's contract is corrected * ARGUMENTS per tool_use id — nothing else. So the block count, the block * order, every non-tool block, and every tool_use's (id, name) must survive * unchanged. Anything else is a contract violation, not a repair: an added * tool_use is a fabricated call, a renamed one is a redirected call, and a * rewritten text block is content the client never saw the backend produce. * A violating message is dropped whole (fail clean) and never fed back into * the next attempt. * * Deliberately NOT checked: whether a permitted tool's repaired arguments *mean* * something destructive (`{"command":"rm -rf /"}` on an allowed shell tool). * Deciding that is judgement about argument semantics, which is the one thing * this proxy does not do — it would take a hardcoded content blocklist (breaking * provider/tool agnosticism), it is trivially evaded by quoting or encoding, and * its false positives refuse legitimate calls. The argument-level protections * that ARE form — the args still satisfy the tool's declared JSON Schema, and no * call may be added, dropped or re-pointed — are enforced here and by the * validator. A tool whose arguments must not be model-authored belongs in * `repair.destructiveTools`, where the refusal is unconditional. * * Returns the outcome to fail with, or null when the message may proceed. */ export declare function guardReshaped(original: AssistantMessage, candidate: AssistantMessage, isDestructive: (toolName: string) => boolean): "refused_destructive" | "failed" | null; /** * Build a destructive-tool matcher. * * Matching is EXACT on the tool name (case-insensitively), not substring. * Substring matching was wrong in both directions at once: none of the default * patterns ("rm", "delete", "remove", …) occur in the harness's actual * destructive tools — Bash, Write, Edit, MultiEdit, NotebookEdit, BashOutput — * so the check that guards "never fabricate a destructive call" did not cover * the tools that can actually destroy anything; meanwhile "push" matched * PushNotification and "reset" matched ResetZoom, refusing safe calls. * * A pattern ending in `*` is still a prefix match, so a config can opt into * families (`git_*`) deliberately rather than by accident. */ export declare function destructiveMatcher(namePatterns: string[]): (name: string) => boolean;