UNPKG

ai

Version:

AI SDK by Vercel - build apps like ChatGPT, Claude, Gemini, and more with a single interface for any model using the Vercel AI Gateway or go direct to OpenAI, Anthropic, Google, or any other model provider.

327 lines (258 loc) • 11.1 kB
--- title: Tool Approvals description: Add manual and automatic approval flows to ToolLoopAgent tools. --- # Tool Approvals By default, tools with an `execute` function run automatically when the model calls them. Use `toolApproval` on `ToolLoopAgent` to review, approve, or deny selected tool calls before they execute. `toolApproval` is useful for tools that can modify data, spend money, execute code, send messages, access private data, or perform any other sensitive action. <Note> `toolApproval` applies to tools executed by the AI SDK. Provider-executed tools run provider-side and do not use AI SDK tool approvals. </Note> ## Statuses Every approval rule returns one of these statuses, either as a string or as an object with a `type` field: - `'not-applicable'`: execute the tool normally without approval metadata. This is the default. - `'approved'`: record an automatic approval, then execute the tool. - `'denied'`: record an automatic denial and return a denied tool output. - `'user-approval'`: emit an approval request and wait for an explicit response. For automatic approvals and denials, use the object form when you want to include a reason: ```ts toolApproval: { deleteFile: { type: 'denied', reason: 'Deleting files is disabled in this workspace', }, } ``` Approval functions can also return `undefined`, which is treated the same as `'not-applicable'`. ## Require Approval for a Tool Use a per-tool map when each tool has a simple policy. ```ts highlight="13-15" import { ToolLoopAgent, tool } from 'ai'; __PROVIDER_IMPORT__; import { z } from 'zod'; const agent = new ToolLoopAgent({ model: __MODEL__, tools: { runCommand: tool({ inputSchema: z.object({ command: z.string() }), execute: async ({ command }) => runCommand(command), }), }, toolApproval: { runCommand: 'user-approval', }, }); ``` When `runCommand` is called, the agent returns a `tool-approval-request` instead of executing the tool. ## Decide Based on Tool Input Use a per-tool approval function when the decision depends on the parsed tool input. The function receives the typed input plus `toolCallId`, `messages`, `toolContext`, and `runtimeContext`. ```ts highlight="17-24" import { ToolLoopAgent, tool } from 'ai'; __PROVIDER_IMPORT__; import { z } from 'zod'; const agent = new ToolLoopAgent({ model: __MODEL__, tools: { processPayment: tool({ inputSchema: z.object({ amount: z.number(), recipient: z.string(), }), execute: async ({ amount, recipient }) => processPayment({ amount, recipient }), }), }, toolApproval: { processPayment: async ({ amount }, { runtimeContext }) => { if (runtimeContext.role !== 'admin') { return { type: 'denied', reason: 'Only admins can send payments' }; } return amount > 1000 ? 'user-approval' : undefined; }, }, }); ``` In this example, non-admin users are denied automatically, large payments require manual approval, and smaller admin payments execute normally. ## Use One Policy for All Tools Pass a function directly as `toolApproval` when approval depends on the full tool call, shared state across tools, or the complete tool set. This is called a `GenericToolApprovalFunction`. ```ts highlight="13-16,18-20,22-23" const agent = new ToolLoopAgent({ model: __MODEL__, tools: { readFile: tool({ inputSchema: z.object({ path: z.string() }), execute: async ({ path }) => readFile(path), }), deleteFile: tool({ inputSchema: z.object({ path: z.string() }), execute: async ({ path }) => deleteFile(path), }), }, toolApproval: ({ toolCall }) => { if (toolCall.dynamic) { return 'user-approval'; } if (toolCall.toolName === 'deleteFile') { return 'user-approval'; } return undefined; }, }); ``` The generic function receives: - `toolCall`: the full tool call, including `toolName`, `toolCallId`, `input`, and whether it is dynamic. - `tools`: all tools available to the model. - `toolsContext`: context for all tools. - `messages`: the messages sent to the model for the step that produced the tool call. - `runtimeContext`: the call's shared runtime context. ## Configure Approval per Request Because `toolApproval` is an agent setting, you can also return it from `prepareCall`. This is useful when approval policy depends on call options, tenant policy, or user permissions. ```ts highlight="7-17" import { ToolLoopAgent, tool } from 'ai'; __PROVIDER_IMPORT__; import { z } from 'zod'; const agent = new ToolLoopAgent({ model: __MODEL__, callOptionsSchema: z.object({ canRunCommands: z.boolean(), }), prepareCall: ({ options, ...settings }) => ({ ...settings, toolApproval: { runCommand: options.canRunCommands ? 'user-approval' : { type: 'denied', reason: 'Command access is disabled' }, }, }), tools: { runCommand: tool({ inputSchema: z.object({ command: z.string() }), execute: async ({ command }) => runCommand(command), }), }, }); ``` ## Handle Manual Approvals Manual approval requires two calls: 1. Call `agent.generate()` or `agent.stream()` with `toolApproval`. 2. Read the `tool-approval-request` from the result or UI stream. 3. Ask the user or your approval system for a decision. 4. Add a `tool-approval-response` to the messages. 5. Call the agent again with the updated messages. ```ts highlight="5-6,10-18,21-24,26" import { type ModelMessage, type ToolApprovalResponse } from 'ai'; const messages: ModelMessage[] = [{ role: 'user', content: 'Delete temp.txt' }]; const result = await agent.generate({ messages }); messages.push(...result.responseMessages); const approvalResponses: ToolApprovalResponse[] = []; for (const part of result.content) { if (part.type === 'tool-approval-request' && !part.isAutomatic) { approvalResponses.push({ type: 'tool-approval-response', approvalId: part.approvalId, approved: true, reason: 'User confirmed the file can be deleted', }); } } messages.push({ role: 'tool', content: approvalResponses, }); const finalResult = await agent.generate({ messages }); ``` If approved, the tool runs on the second call. If denied, the model receives the denial and can respond without the tool result. <Note> When a tool execution is denied, consider adding an instruction such as "When a tool execution is not approved, do not retry it" to prevent repeated approval requests for the same action. </Note> ## Use with `useChat` When streaming an agent to a chat UI, approval requests appear as tool parts with `state: 'approval-requested'`. Respond with `addToolApprovalResponse`. ```tsx highlight="7-9,17-40" 'use client'; import { useChat } from '@ai-sdk/react'; import { lastAssistantMessageIsCompleteWithApprovalResponses } from 'ai'; export default function Chat() { const { messages, addToolApprovalResponse } = useChat({ sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses, }); return messages.map(message => message.parts.map(part => { if (part.type !== 'tool-runCommand') { return null; } if (part.state === 'approval-requested' && !part.approval.isAutomatic) { return ( <div key={part.toolCallId}> <button onClick={() => addToolApprovalResponse({ id: part.approval.id, approved: true, }) } > Approve </button> <button onClick={() => addToolApprovalResponse({ id: part.approval.id, approved: false, }) } > Deny </button> </div> ); } }), ); } ``` Only call `addToolApprovalResponse` for manual approvals. Automatic approvals and denials already include approval state in the stream. ## Security Considerations ### Trust model In the standard `useChat` pattern, the server rebuilds the conversation from the messages the client sends each turn. The server does not persist conversation state between requests. This means the message history is client-controlled input. Tool approvals reconstructed from this history are re-validated before execution: the tool input is checked against the tool's schema, and the approval policy is re-evaluated. However, without additional protection, a client that crafts a valid-looking approval for a schema-conforming input can bypass the human-in-the-loop step. If your tools perform sensitive operations (modifying data, spending money, calling external APIs, accessing private resources), use `experimental_toolApprovalSecret` to cryptographically bind approvals to the server that issued them. ### Signing approvals with `experimental_toolApprovalSecret` When you provide a secret, the server HMAC-signs each approval request at issuance and verifies the signature when the approval is replayed. A forged or tampered approval is rejected before the tool executes. ```ts highlight="5" const result = await streamText({ model: __MODEL__, tools: { deleteFile, runQuery }, toolApproval: { deleteFile: 'user-approval', runQuery: 'user-approval' }, experimental_toolApprovalSecret: process.env.TOOL_APPROVAL_SECRET, messages, }); ``` The signature binds the approval to the exact tool name, tool call ID, and input arguments. Changing any of these after signing invalidates the approval. **Setting up the secret:** 1. Generate a high-entropy random string (at least 32 bytes): ```bash openssl rand -base64 32 ``` 2. Store it as an environment variable accessible to all server instances: ``` TOOL_APPROVAL_SECRET=your-generated-secret-here ``` 3. Pass it to `generateText` or `streamText` via `experimental_toolApprovalSecret`. Every serverless instance that might handle a request needs the same secret, since one instance signs the approval and a different instance may verify it on the next turn. **Behavior when configured:** - Approval requests without a valid signature are rejected (fail-closed) - No secret configured: approvals work as before (backward compatible) - The secret is never sent to the client or included in the stream <Note> `experimental_toolApprovalSecret` is not yet supported on `WorkflowAgent`. For durable workflows, the workflow runtime provides its own persistence layer that can be used to verify approvals. </Note> ## Related APIs - Use `toolApproval` with `ToolLoopAgent`, `generateText`, and `streamText`. - Author approval rules as code with [Policy-Based Tool Approvals](/docs/agents/policy-tool-approvals) (`@ai-sdk/policy-opa`). - Use `needsApproval` only with [`WorkflowAgent`](/docs/agents/workflow-agent), where approvals suspend and resume durable workflow execution. - Subagent tools cannot use `toolApproval`; see [Subagents](/docs/agents/subagents#no-tool-approvals-in-subagents).