/** * `requireApproval` — ergonomic HITL shorthand on `defineFunctionTool` * (S4). Closes the gap identified by the 2026-05-21 ADK comparison * (see `docs/sdk2/research/2026-05-21-google-adk-vs-qodo-ts-sdk.md`, * Opportunity 5 — ADK's `FunctionTool(my_fn, require_confirmation=True)` * is one line; qodo-ts-sdk's pre-S4 surface required ~10 lines across * annotations + name-based gate + `onRequest` resolution). * * The flag composes onto the existing three-layer HITL model documented * in `docs/sdk2/concepts/03-hitl.md`: * * - **Layer 1 (instruction).** `defineFunctionTool({ description })` * already shapes the LLM's choice to call the tool. The optional * `prompt` on `ApprovalConfig` becomes the human-facing prompt at * resolution time — not an instruction to the model. * - **Layer 2 (schema).** QAR currently owns `HITLBlock` synthesis on * the wire (name-based, server-side, deployment-driven). S4 does * NOT attempt to push a synthetic `HITLBlock` onto QAR's * `tool.request` envelope; doing so would require a wire-side ADR * and a QAR change. Instead, S4 synthesizes the equivalent * decision-making flow SDK-side at dispatch time. * - **Layer 3 (trust-tier gate).** The actual gate moves into the SDK * for tools the consumer marked with `requireApproval`. Before the * handler runs, the router asks the consumer's * `ApprovalHandler` to approve / deny. Approve → handler invoked * with the parsed Zod args. Deny → `outcome: 'denied'` returned to * the LLM. Denial is a recoverable error (not a pipeline-fatal one); * the agent loop sees the rejection as a normal tool-result and can * decide whether to retry, ask the user, or give up. * * Annotation passthrough is **advisory**, not the gate. The MCP spec * explicitly tags `Tool.annotations` as untrusted (see * `docs/sdk2/concepts/03-hitl.md` § Dynamic HITL); the runtime's * name-based policy is what holds when an MCP misbehaves. S4 still * merges `sensitivity`-driven hints (`destructiveHint`, `openWorldHint`) * onto the produced `FunctionToolDef.function.annotations` so downstream * observability (OTel span attrs, Shape C dynamic filters reading * `annotations.destructiveHint`) can see them — but the consumer-side * approval flow IS the SDK-level gate. */ import type { JsonObject } from '../../qar/json.js'; import type { ToolCall, ToolResponseResult } from '../../qar/payloads.js'; import type { McpToolAnnotations } from '../../qar/specs.js'; /** * Sensitivity tier carried on an {@link ApprovalConfig}. Picks the * advisory annotation overlay applied to the produced * `FunctionToolDef.function.annotations`: * * - `'low'` — no overlay. The flag still gates dispatch through the * consumer's `ApprovalHandler`; advisory annotations remain at * whatever the consumer passed in. * - `'pii'` — adds `destructiveHint: true`. Signals "this tool sees * user data; treat the call site carefully" to downstream filters. * - `'secret'` — adds `destructiveHint: true` AND `openWorldHint: * true`. Signals "this tool touches external state or secret-tier * data". Equivalent (semantically) to QAR's * `Sensitivity.SECRET` HITL classification — see * `docs/sdk2/concepts/03-hitl.md` § Bubble routing. * * Sensitivity is purely the annotation overlay knob; it does NOT * change SDK-side dispatch logic. The same approval flow fires * regardless of tier — the tier is metadata for downstream consumers. */ export type ApprovalSensitivity = 'low' | 'pii' | 'secret'; /** * UX shape requested at the approval surface. Mirrors the three * `HITLBlock` kinds from `docs/sdk2/concepts/03-hitl.md`: * * - `'approval'` — yes/no decision. The {@link ApprovalDecision}'s * `decision` discriminates `'approve'` vs `'deny'`; the optional * `reason` carries free-form text. Default kind when * `requireApproval: true` is set without an explicit * {@link ApprovalConfig}. * - `'choice'` — closed-set picker. The {@link ApprovalHandler} * returns `{ decision: 'approve', selection: '' }` when * a positive option is chosen and `{ decision: 'deny' }` otherwise. * S4 does not parameterize the option list — for a custom option * set, drop down to the manual `onRequest` shape (full HITL * control). `'choice'` here is a hint to the handler that the * consumer expects a closed-set UI. * - `'input'` — free-form text response. The handler returns * `{ decision: 'approve', input: '' }`; the text flows back * to the underlying function-tool handler via the * {@link ApprovalDecision.input} field so the handler can pass it * forward when invoked. * * The kind is an authoring-time hint to the {@link ApprovalHandler} — * it does not change the dispatch wire (no `HITLBlock` is synthesized * onto QAR's envelope). */ export type ApprovalKind = 'approval' | 'choice' | 'input'; /** * Configuration for a `requireApproval`-gated tool. Every field is * optional; passing `requireApproval: true` (the boolean form) is * equivalent to passing * `{ kind: 'approval', sensitivity: 'low' }`. * * Fields are advisory hints to the {@link ApprovalHandler} the * consumer registers via `client.tools.setApprovalHandler(...)`. The * SDK passes the full {@link ApprovalConfig} through unchanged so * production handlers can render richer UIs (custom prompts, * deadline-driven escalation, sensitivity-keyed audit routing). */ export interface ApprovalConfig { /** * UX shape requested at the resolver. See {@link ApprovalKind}. * Defaults to `'approval'`. */ readonly kind?: ApprovalKind; /** * Sensitivity tier. Drives the annotation overlay applied to the * produced `FunctionToolDef.function.annotations` (advisory). Does * not change SDK-side dispatch logic. Defaults to `'low'`. */ readonly sensitivity?: ApprovalSensitivity; /** * Soft deadline (ms) the handler should observe before escalating * or auto-denying. The SDK does NOT enforce this timeout (a * blocking stdin prompt happily blocks past it); the value is * passed through to the handler so production overrides can wire * their own timer. */ readonly deadlineMs?: number; /** * Human-facing prompt rendered at the resolver. Defaults to a * canned string built from the tool name and arguments. Useful * when the tool's `description` is too verbose for an approval UI. */ readonly prompt?: string; } /** * Decision returned by an {@link ApprovalHandler}: * * - `{ decision: 'approve' }` — the handler runs with the original * parsed args. Optional `input` is passed through to the handler * on the per-call context for `'input'`-kind approvals. * - `{ decision: 'deny' }` — the handler does NOT run; the SDK * returns `{ outcome: 'denied' }` to QAR with the optional * `reason` as the error message. * * Throwing from the handler is also valid — every throw maps to * `outcome: 'denied'` (`ToolDeniedError`) or `outcome: 'error'` (any * other Error). Use throws for "the resolver UI broke", returns for * "the resolver decided". */ export type ApprovalDecision = { readonly decision: 'approve'; /** Optional free-text input for `'input'`-kind approvals. */ readonly input?: string; /** Optional selection identifier for `'choice'`-kind approvals. */ readonly selection?: string; } | { readonly decision: 'deny'; /** Optional rejection reason; surfaces as the `outcome: 'denied'` error message. */ readonly reason?: string; }; /** * Per-call payload passed to an {@link ApprovalHandler}. Carries * everything the resolver needs to render the prompt: the requested * UX shape ({@link ApprovalConfig}), the inbound `ToolCall`, and the * originating graph node name. */ export interface ApprovalRequest { /** The tool name being gated. Matches `FunctionToolDef.function.name`. */ readonly toolName: string; /** The full approval configuration declared on the tool. */ readonly config: ApprovalConfig; /** The inbound `ToolCall` (raw args, `tool_call_id`, optional HITL block). */ readonly call: ToolCall; /** * Zod-parsed argument bag — the same shape the function-tool * handler would receive on approve. Reflects `.default()` / * `.transform()` / `.refine()` chains applied by the consumer's * schema, so the approver sees the canonical args the handler will * actually run with (not the raw wire bag). The default * {@link ApprovalRequest.prompt} renders against this when present; * falls back to {@link ToolCall.arguments} only if Zod parsing * isn't yet available (e.g. when `resolveApproval` is invoked * outside the router for unit tests). */ readonly parsedArguments?: JsonObject; /** Originating graph node name (echoed back on `tool.response`). */ readonly nodeName: string; /** * Pre-rendered human prompt — the `config.prompt` value when set, * else a canned `"Approve ()?"` string. Handlers can * render this verbatim or build their own from the parts above. */ readonly prompt: string; } /** * Handler signature for `client.tools.setApprovalHandler(...)`. * * Returns synchronously or asynchronously. Throws map to denials * (`ToolDeniedError` → `outcome: 'denied'`) or errors (`outcome: * 'error'`) per the function-tool handler contract. */ export type ApprovalHandler = (req: ApprovalRequest) => ApprovalDecision | Promise; /** * Default approval handler. Behavior: * * - If `process.stdin.isTTY` (interactive Node CLI) — prompt via * `readline` with a yes/no question. `y` / `yes` → approve; * anything else → deny. Useful for tutorials, scripts, * `examples/usecases/*`. * - Otherwise (non-TTY, daemon, web worker, browser bundle, CI) — * deterministically deny with a typed error message so the * consumer's stack trace points at the missing * `setApprovalHandler` registration instead of getting a * mysterious "tool denied" from QAR. Auto-approving in * non-interactive contexts would silently defeat the gate — the * wrong default for an approval surface. * * Override via `client.tools.setApprovalHandler(fn)`. Production * apps that route approvals to Slack / a web UI / a pager should * register an explicit handler at startup; the default is the * "I have an stdin" fallback for the simplest cases. * * **Concurrency.** `FunctionToolRouter.dispatch` fires per-call * handlers via `Promise.all`. Multiple `requireApproval`-gated * calls in one `tool.request` batch would otherwise open multiple * readline interfaces on the same `process.stdin` and interleave — * see {@link readlineApprovalPrompt} for the module-level * promise-chain mutex that serializes the default stdin prompts * (regression test in `tests/require-approval.test.ts` * § readlineApprovalPrompt mutex). * Consumer-override handlers (Slack, web UI) don't share stdin and * stay parallel by design — serializing them would hurt batch * latency in production. */ export declare const defaultApprovalHandler: ApprovalHandler; /** * Resolve an approval request against the supplied handler. Maps * thrown errors (handler bugs, denial-by-throw) to the wire * `ToolResponseResult` shape: * * - Handler returns `{ decision: 'approve' }` → returns the * {@link ApprovalDecision} so the caller can pass through the * optional `input` / `selection` payload to the function-tool * handler. * - Handler returns `{ decision: 'deny' }` → returns the * pre-formatted `{ outcome: 'denied' }` response item. The * caller MUST NOT invoke the function-tool handler. * - Handler throws `ToolDeniedError` → mapped to `outcome: 'denied'`. * - Handler throws any other Error → mapped to `outcome: 'error'` * with the error message (capped by the dispatcher's existing * `MAX_ERROR_LEN` boundary). * * The two-arm return shape (`'approve' | { outcome: 'denied' | 'error' }`) * lets the caller skip the function-tool handler invocation entirely * on rejection — no need for an `if (decision === 'deny')` follow-up * branch. */ export declare function resolveApproval(handler: ApprovalHandler, toolName: string, config: ApprovalConfig, call: ToolCall, nodeName: string, parsedArguments?: JsonObject): Promise; /** * Merge the consumer-supplied `annotations` (if any) with the * sensitivity-driven advisory overlay. Consumer values WIN on * collision — `requireApproval` is a shorthand, not a forced * override. Set sensitivity to opt into the overlay; leave * `annotations` undefined to accept it verbatim. * * Returns `undefined` when neither side contributes a field * (so the produced `FunctionToolDef` stays free of an empty * `annotations: {}` object — every wire byte counts). */ export declare function mergeApprovalAnnotations(base: McpToolAnnotations | undefined, config: ApprovalConfig): McpToolAnnotations | undefined; /** * Normalize the consumer-facing `requireApproval` value into the * internal {@link ApprovalConfig} shape. `true` → `{ kind: * 'approval', sensitivity: 'low' }`; an explicit * {@link ApprovalConfig} is returned as-is; `false` / `undefined` * returns `undefined` (gate disabled). * * Centralizing the normalization here keeps `defineFunctionTool` and * the router from re-implementing the boolean → object widening. */ export declare function normalizeApprovalConfig(value: boolean | ApprovalConfig | undefined): ApprovalConfig | undefined; //# sourceMappingURL=approval.d.ts.map