import type { DocumentContent, ImageContent, TextContent } from "../internal/llm.js"; import type { PermissionResult, ToolCallRequest } from "./tool-policy.js"; /** * In-process hook seam (design/37) — a small, provider-agnostic interception layer modeled on CC's * hooks but reduced to three process-internal callbacks (no shell/HTTP executors, no settings files): * * - {@link Hooks.preToolUse} runs before a tool executes. It may **rewrite** the args (an `allow` with * `updatedInput`), **restrict** the call (`deny`/`ask`), and/or inject `additionalContext`. It is a * pre-filter only: a hook's `allow` does NOT bypass the tool-policy gate — the policy still runs on * the (possibly rewritten) args and has the final say (load-bearing invariant; see {@link runToolGate}). * - {@link Hooks.postToolUse} runs after a tool executes. It may replace the result content * (`updatedOutput`) and/or append `additionalContext`. * - {@link Hooks.userPromptSubmit} runs before the objective becomes a user message. It may `block` * submission (the task fails with a model-readable reason) or inject `additionalContext` ahead of it. * * All feedback the model should see (a block reason, injected context) is wrapped as a * `` via {@link formatHookFeedback} so the model can read and self-correct. */ export interface Hooks { preToolUse?(toolName: string, input: unknown, ctx: HookToolContext): PreToolUseResult | undefined | Promise; postToolUse?(toolName: string, input: unknown, output: HookToolOutput, ctx: HookToolContext): PostToolUseResult | undefined | Promise; userPromptSubmit?(prompt: string): UserPromptSubmitResult | undefined | Promise; /** * roadmap #5 (CC 198 Stop hook, :473831-): runs when the agent WOULD OTHERWISE END its run (no * more tool calls, steering and follow-up queues dry). Return `{ block: reason }` to PUSH BACK — * the reason is injected as a model-readable message and the run continues another turn (external * adjudication: "the tests still fail", "the deliverable is missing X"). Return `undefined` to let * the run end. Guardrails (CC-exact): consecutive blocks are capped (default 8 — the loop then * overrides and ends with a warning); `ctx.stopHookActive` is true when the run is ALREADY * continuing due to a previous block — a hook that ignores it and blocks unconditionally loops * until the cap. `maxTurns` still outranks everything. */ stop?(ctx: StopHookContext): StopHookResult | undefined | Promise; /** * design/134 (CC PostToolUseFailure parity, SDK 0.3.202): runs after a tool EXECUTION fails — * mutually exclusive with {@link postToolUse} (success → postToolUse; failure → this). Fires ONLY * for genuine execution failures: a gate/policy block, a plan-mode deny, a pre-execution abort or a * durable suspend triggers NEITHER callback (CC scopes those to the PermissionDenied event domain, * not implemented here). Capability is additionalContext-only (CC-exact): the text is appended to * the error tool result as a ``; the failure itself cannot be rewritten. */ postToolUseFailure?(toolName: string, input: unknown, failure: HookToolFailure, ctx: HookToolContext): PostToolUseFailureResult | undefined | Promise; /** * design/134 (CC PostToolBatch parity, SDK 0.3.202): fires ONCE after every tool call in a batch * (one assistant turn's calls, design/120 stream-inline included) has resolved, before the next * model request. Per-tool postToolUse/postToolUseFailure fire first; the batch callback fires after, * at the turn boundary. A zero-tool turn does not fire; an aborted/steered-away boundary does not * fire (same gate family as design/133 attachments). additionalContext is injected once for the * whole batch as a boundary `` (sanitized, shares the attachment byte cap). */ postToolBatch?(batch: PostToolBatchCall[]): PostToolBatchResult | undefined | Promise; /** * design/134 (CC PreCompact parity): runs before each compaction, AFTER the trigger gate and a * valid cut point are confirmed (so every preCompact corresponds to a compaction that would * actually happen) and before the summary LLM call. `trigger`: "auto" (threshold) | "manual" * (/compact) | "forced" (promptTooLong recovery and trim-pressure — compaction is not optional * there). Return `{ block }` to skip this compaction (honored on auto/manual ONLY; ignored with a * trace on forced — blocking a compaction the provider already demanded would kill the run). * `additionalInstructions` is merged (appended) into the summarization instructions (CC * mergeHookInstructions shape). A THROWING preCompact is swallowed on every path (traced, treated * as no-block): an observer bug must never feed the compaction breaker or kill PTL recovery. */ preCompact?(ctx: PreCompactContext): PreCompactResult | undefined | Promise; /** * design/134 (CC PostCompact parity): observe-only, after the compaction landed (summary appended * to the session). No return capability (CC-exact); a throw is swallowed and traced. */ postCompact?(ctx: PostCompactContext): void | Promise; /** * StopFailure (the last engine-owned event of the registry-core 0.1.51 hooks contract; CC semantics: * "the turn ended because of an API error"). Fires ONCE, after the result is assembled, when the task * terminates `failed` BECAUSE the model/API layer errored (a brain error terminal — * auth / network / rate_limit / server_error / …). It does NOT fire for engine-owned failure classes: * budget, timeout/abort, max-turns, degenerate/walltime cutoffs (deliberate engine cuts), storage * conflict, blocked, or a durable suspend — those are not API errors. Observe-only (CC-exact: the * contract defines no StopFailure output capability); a throwing callback is swallowed + traced * (design/134 discipline — an observer bug must never rewrite a terminal state). */ stopFailure?(ctx: StopFailureContext): void | Promise; /** * design/134 R5 (CC PermissionDenied parity, SDK 0.3.202 — service [418]③): runs when the tool gate's * adjudicate chain DENY-SHORT-CIRCUITS a tool call — a ToolPolicy deny, a gate tighten (egress / * irreversibility / coarse shellGate) whose `ask` resolved to deny (headless auto-deny included), or the * plan-mode write-deny. Closes the observation blind spot where a blocked call fires NEITHER postToolUse * NOR postToolUseFailure (R3 scoping). Does NOT fire for: * - a **durable suspend** (the ask path — CC surfaces that via a `can_use_tool` control_request); * - a **PreToolUse hook deny** (CC-exact: "PreToolUse hook denies bypass canUseTool and are not * covered here"). * Observe-only v1 (CC-exact payload `{toolName, input, toolCallId, reason}` + our `source` enum standing * in for CC `decision_reason_type`): no `retry` capability (recorded — retry semantics vs our adjudicate * chain need their own design), no additionalContext. A throwing callback is swallowed + traced via * `onError(phase:"hook")` — an observer bug must never alter the deny outcome (design/134 discipline). */ permissionDenied?(payload: PermissionDeniedPayload): void | Promise; } /** * Where a {@link Hooks.permissionDenied} deny came from — our gate-source enum standing in for CC's * `decision_reason_type` ('classifier'|'asyncAgent'|'mode'|'rule'), named after OUR adjudicate-chain * sources (design/134 R5): * - `"policy"` — the ToolPolicy denied directly, or a policy-raised `ask` resolved to deny * (headless auto-deny / approver said no / approval aborted). * - `"hook"` — a PreToolUse hook `ask` (folded allow→ask) resolved to deny. (A hook's own `deny` * short-circuit is EXCLUDED from the event, CC-exact — this covers only the hook-ask path.) * - `"safety"` — the gate's deterministic egress/irreversibility tighten (design/70 / design/77 §4) * raised the `ask` that resolved to deny, from an explicit per-tool mark. * - `"shellGate"` — same tighten-deny, but the tool's irreversibility tier was installed by the COARSE * `TaskSpec.shellGate` doctrine (design/80 D-E), not an explicit per-tool mark. * - `"planMode"` — the design/108 plan-mode write-deny short-circuit (a read-only fidelity gate). */ export type PermissionDeniedSource = "policy" | "hook" | "safety" | "shellGate" | "planMode" | "classifier"; /** The payload a {@link Hooks.permissionDenied} callback observes (CC-exact fields + `source`). */ export interface PermissionDeniedPayload { toolName: string; /** The FINAL (post-hook-rewrite / post-policy-rewrite) args the chain adjudicated — what would have * executed; not necessarily the model's original args. */ input: unknown; toolCallId: string; /** Human/model-readable deny reason (CC `reason`) — the RAW decision text, NOT ``-wrapped. */ reason: string; /** Which gate source produced the deny (our `decision_reason_type` analog). */ source: PermissionDeniedSource; } /** * 1.256 复审 MED-1 — observe-only payload isolation for {@link Hooks.permissionDenied}: clone the tool * args before they ride the observer payload, so a hook mutating `payload.input` can never pollute the * LIVE args object (later events / audit records share it). Same posture as the postToolUseFailure * details clone (prepare-task): `structuredClone` first; a non-structured-cloneable graph * (functions/handles) falls back to a SHALLOW plain object/array copy (top-level mutation isolated); * a non-object primitive passes through as-is (immutable anyway). */ export declare function cloneObserverInput(input: unknown): unknown; /** Context for {@link Hooks.stopFailure} — aligned with the TaskResult error face (observe-only). */ export interface StopFailureContext { /** Human-readable error message (the assembled `errorMessage`; any `[code]` prefix already stripped). */ error: string; /** Machine-readable kind — the assembled `errorCode` lifted from the brain's `[code]` prefix * (auth / network / rate_limit / server_error / …); absent when the provider stamped no code. */ errorKind?: string; /** Turns completed when the failure ended the run. */ turns: number; } /** The failed tool execution a PostToolUseFailure callback inspects (design/134). */ export interface HookToolFailure { /** Model-facing error text of the failed call (the error tool result's text content, joined). */ error: string; /** Wide any-abort semantics (⊇ CC `is_interrupt`, which is user-interrupt specifically): true when * the task abort signal fired — timeout, budget, cancel or suspend — not only a human interrupt. */ isInterrupt: boolean; /** Tool execution time in ms when the engine has it (CC 0.3.202 `duration_ms` parity); else absent. */ durationMs?: number; /** Element-level clone of the error result's content blocks (mutating them cannot rewrite the * transcript — the failure is observe-only). */ content: Array; /** BEST-EFFORT deep clone (structuredClone) of the tool's raw details. When the details graph is * not structured-cloneable (functions/handles), this is the LIVE reference — treat it as * READ-ONLY: a mutation would corrupt the result the loop is about to commit. */ details: unknown; } /** A PostToolUseFailure result: append model-readable context to the error result (CC-exact, no rewrite). */ export interface PostToolUseFailureResult { additionalContext?: string; } /** One resolved call in a postToolBatch payload (CC PostToolBatchToolCall shape, thin projection — * `response` is a bounded text digest of the result, never the full details object). */ export interface PostToolBatchCall { toolName: string; input: unknown; toolCallId: string; response?: string; isError: boolean; } /** A PostToolBatch result: inject context once for the whole batch at the turn boundary. */ export interface PostToolBatchResult { additionalContext?: string; } /** Context for {@link Hooks.preCompact} (design/134). */ export interface PreCompactContext { /** "auto" = threshold-triggered; "manual" = /compact; "forced" = promptTooLong recovery or * trim-pressure propagation (block is ignored on forced — the compaction is not optional). */ trigger: "auto" | "manual" | "forced"; /** The summarization instructions in effect (spec/deployment-level), when set. */ customInstructions?: string; } /** A PreCompact result: skip this compaction (auto/manual only) and/or extend the summary instructions. */ export interface PreCompactResult { /** When set (and trigger is not "forced"), this compaction is skipped; the reason is traced. */ block?: string; /** Appended to the summarization instructions for this compaction (never replaces them). * LLM-summary path only: a compaction served from a `summaryProvider` reuse (Seam C) never runs * the summarization call, so these instructions are not consumed there (recorded design gap — * extending the provider contract is out of scope for design/134). */ additionalInstructions?: string; } /** Context for {@link Hooks.postCompact} (design/134, observe-only). */ export interface PostCompactContext { trigger: "auto" | "manual" | "forced"; /** The conversation summary the compaction produced (CC `compact_summary` parity). */ summary: string; tokensBefore?: number; tokensAfter?: number; } /** Context for the {@link Hooks.stop} hook (CC `stop_hook_active` parity). */ export interface StopHookContext { /** True when this run is already continuing because a previous stop() blocked — check it and * return success (undefined) once your condition can't be improved, or you will loop to the cap. */ stopHookActive: boolean; /** Consecutive blocks so far in this run (resets when a stop() call lets the run proceed). */ consecutiveBlocks: number; } /** A Stop hook result: block the run from ending, with a model-readable reason. */ export interface StopHookResult { /** When set, the run does NOT end: this reason is injected (as a ``) and the * model gets another turn to address it. */ block?: string; /** * CC 2.1.201 parity (CC :472050-472077): extra model-readable context injected at the stop point — * DECOUPLED from `block`. When present it is injected (as a ``, neutral framing: * "Stop hook additional context: …") whether or not the hook blocked; `additionalContext` WITHOUT * `block` still continues the run one more turn, but does NOT count toward the consecutive-block * cap (the cap is driven by `block` alone). */ additionalContext?: string; } /** Identifying context passed to tool hooks. */ export interface HookToolContext { toolCallId: string; toolName: string; } /** The executed tool result a PostToolUse hook inspects. */ export interface HookToolOutput { content: Array; details: unknown; isError: boolean; } /** * A PreToolUse hook result: a {@link PermissionResult} (so a hook can `deny`/`ask`, or `allow` with an * `updatedInput` rewrite) plus optional `additionalContext` injected into the eventual tool result. */ export type PreToolUseResult = PermissionResult & { additionalContext?: string; }; /** A PostToolUse hook result: replace the tool output and/or append context (both optional). */ export interface PostToolUseResult { /** If provided, replaces the tool result content array in full. */ updatedOutput?: Array; /** Appended to the result as a `` the model can read. */ additionalContext?: string; } /** A UserPromptSubmit hook result: block the submission, or inject context ahead of the prompt. */ export interface UserPromptSubmitResult { /** Block submission entirely; the task fails with this model-readable reason. */ block?: string; /** Injected ahead of the user's prompt (wrapped as a ``). */ additionalContext?: string; } /** Wrap model-facing hook/gate feedback in a `` so it reads as guidance, not data. * NOTE (council design/74 #6): this does NOT escape a literal `` in `text` — callers MUST * pass trusted, first-party strings (every current caller does: fixed gate/limit messages). If a future * caller needs to relay UNTRUSTED content (tool output, user data), it must sanitize the close tag first * (or use the `delimitUntrusted` fence), or a crafted payload could break out of the reminder framing. */ export declare function formatHookFeedback(text: string): string; /** The outcome of the two-phase tool gate, mapped onto the harness `tool_call` hook return shape. */ export interface ToolGateResult { /** Block execution (the loop emits an error tool result with `reason`). */ block?: boolean; /** Model-readable block reason (already ``-wrapped). */ reason?: string; /** Rewritten args to execute with (re-validated by the loop); omitted when nothing rewrote. */ updatedInput?: unknown; /** * design/45: the gate routed a policy `ask` to a **durable suspension** (no synchronous approver). * `suspendAsk` already persisted the checkpoint and aborted the run; the caller records this so the * task assembles as `status:"suspended"` with the token. Suspend lives ONLY here in the gate's `ask` * branch — it never becomes a `PermissionResult` action (policy stays pure allow/ask/deny, §15.2). */ suspend?: { token: import("./checkpoint-store.js").CheckpointToken; gate: import("./checkpoint-store.js").CheckpointGate; }; /** PreToolUse `additionalContext` to append to this call's eventual tool result (correlated by id). */ preToolContext: string[]; } /** Inputs to the two-phase tool gate. `adjudicate`/`resolveAsk` are pre-bound to the task abort signal. */ export interface ToolGateInput { event: { toolCallId: string; toolName: string; input: Record; }; preToolUse?: Hooks["preToolUse"]; /** The combined tool-policy check (abort-bound), or undefined when no policy is wired (→ allow). */ adjudicate?: (req: ToolCallRequest) => Promise; /** Resolve an `ask` to allow/deny via `onAsk` (abort-bound). Required iff a decision can be `ask`. */ resolveAsk: (decision: PermissionResult, req: ToolCallRequest) => Promise; /** * design/45: route a policy `ask` to a **durable suspension** instead of the synchronous `resolveAsk` * (onAsk) path. Called in the `ask` branch with the FINAL post-hook args; if it returns a suspend * directive it has already persisted the checkpoint + aborted the run, and the gate short-circuits to * `{ suspend }`. Returns `undefined` to fall through to `resolveAsk` (non-durable / capture failed). * Omitted entirely → no durable mode (the 1.63 synchronous path, unchanged). */ suspendAsk?: (req: ToolCallRequest, postHookArgs: unknown, /** design/80 D-2: present (non-undefined) ONLY when this ask is a SAFETY tighten (the egress/ * irreversibility gate tightened a surviving `allow` to `ask`, `decisionReason === "safety"`). Drives the * mint-site gate-kind choice — a safety tighten mints `irreversible_ask` (non-budgetable) EVEN WHEN * `durableApproval` is wired. The object records which axis(es) tightened (persisted as the gate's * `safetyAxis`). Absent for a plain policy/hook `ask` (→ the normal `human`/`irreversible_ask` choice). */ safety?: import("./checkpoint-store.js").SafetyAxis) => Promise; /** * design/70: the called tool is egress-marked (`ToolSpec.egress` — an external write: open a PR, * push, send). The gate tightens a surviving `allow` to `ask` so an egress tool is NEVER * auto-allowed — execution always passes an explicit ask-resolution (onAsk / durable suspend); * headless resolves to deny. Filled by the caller from the tool's spec; policies stay egress-blind. */ egress?: boolean; /** * design/77 §4 (Gate 4): the called tool's irreversibility tier (`ToolSpec.irreversibility`), filled by * the caller from the tool's spec. After the egress tighten, the gate runs a SECOND deterministic tighten: * `"always"` tightens a surviving `allow` to `ask`; `"maybe"` calls {@link reversibilityProbe} and tightens * UNLESS the probe reports reversible (fail-closed on timeout/throw); `"never"`/undefined → untouched. * Policies stay irreversibility-blind; the gate is the single chokepoint. */ irreversibility?: "never" | "maybe" | "always"; /** * design/77 §4: the `"maybe"`-tier probe from the tool's spec (read at prepare-time, captured here — not a * tool argument). Called ONLY when `irreversibility === "maybe"` && the surviving decision is `allow`, * time-bounded by {@link approvalTimeoutMs}. Fail-closed: anything other than `{ reversible: true }` * (including a timeout or a throw) tightens to `ask`. A probe is never trusted to AUTO-ALLOW past the gate. */ reversibilityProbe?: (args: unknown) => { reversible: boolean; } | Promise<{ reversible: boolean; }>; /** design/77 §4: deadline (ms) for {@link reversibilityProbe}; on timeout the gate fails closed to `ask`. */ approvalTimeoutMs?: number; /** design/77 §4: the task abort signal — bounds {@link reversibilityProbe} by the task's real deadline * (timeout/cancel) in addition to {@link approvalTimeoutMs}; an abort while probing fails closed to `ask`. */ abortSignal?: AbortSignal; /** * design/134 R5: observer for the gate's DENY short-circuit ({@link Hooks.permissionDenied}), pre-wrapped * by the caller (swallow + onError) so it never throws. Fired ONCE, at the single deny exit of the * adjudicate chain — a policy deny or a resolved-ask deny. NOT fired for a PreToolUse hook deny * (CC-exact exclusion) or a durable suspend (the ask path). runToolGate still try/catches defensively: * an observer must never alter the deny outcome. */ permissionDenied?: (payload: PermissionDeniedPayload) => void | Promise; /** * design/134 R5: true when this tool's `irreversibility` tier was installed by the COARSE * `TaskSpec.shellGate` doctrine (design/80 D-E `shellGatedBash`/`shellGatedMonitor`), not an explicit * per-tool mark — attributes a tighten-deny to `source:"shellGate"` instead of `"safety"`. */ shellGated?: boolean; /** * design/143 批2 ([672]-A, CC 2.1.207 auto mode): when present, a surviving `ask` is routed to the * small-model policy CLASSIFIER before any human/durable resolution: * - verdict `allow` → the ask resolves to allow (`decisionReason:"classifier"`) — no suspend, no onAsk; * - verdict `block` → deny (`decisionReason:"classifier"`, `source:"classifier"`; CC `automode-blocked`); * - `unavailable`/`parse_error` → FAIL-CLOSED: the ask continues down the ORIGINAL chain * (durable suspend / onAsk / headless auto-deny) exactly as if auto mode were absent * (CC `automode-unavailable`/`automode-parsing-error` — "NOT a policy decision"). * SCOPE (declared semantic): this covers EVERY surviving ask, INCLUDING the deterministic egress/ * irreversibility safety tightens — auto mode is the deployment's explicit choice to let the * classifier be the "explicit ask-resolution" for this session (its rule set covers exactly those * action classes: outbound writes, irreversible operations, and sensitive-value handling). A * deployment that wants human-only * resolution for safety asks simply does not arm auto mode. The classifier can never AUTO-ALLOW by * failing — only an affirmative `no` allows. */ autoMode?: { decider: import("./auto-mode.js").AutoModeDecider; }; } /** * The design/37 **two-phase tool gate** — the single chokepoint that makes the load-bearing invariant * structural ("a hook's `allow` cannot bypass the policy's `deny`/`ask`"): * * 1. **collect** — run the PreToolUse hook, threading any `updatedInput` rewrite into `currentInput` * and collecting `additionalContext`. A hook `deny` short-circuits to a block immediately; a hook * `ask` is remembered (it does not short-circuit — a later policy `deny` outranks it). * 2. **adjudicate** — run the tool policy on the FINAL `currentInput` (never the model's stale args), * fold it with any remembered hook-ask via `deny > ask > allow`, and resolve a surviving `ask` * through `onAsk`. The policy ALWAYS runs regardless of the hook's verdict. A policy `allow` may * itself carry an `updatedInput` rewrite (redact/clamp), applied last over any hook rewrite. * * Because both phases run in this one linear function, the ordering — and the "policy is final" * invariant — is enforced by the call stack, not by registration convention. Returns the gate result * for the harness `tool_call` hook plus the PreToolUse context to attach to the tool result. */ export declare function runToolGate(input: ToolGateInput): Promise; //# sourceMappingURL=hooks.d.ts.map