Guides
Tool Approvals
By default, tools with an `execute` function run automatically when the model calls them. Use `toolApproval` on `runAgentLoop` to review, approve, or deny selec
Tool Approvals
By default, tools with an execute function run automatically when the model calls them. Use toolApproval on runAgentLoop to review, approve, or deny selected tool calls before they execute.
Tool approvals are useful for tools that can modify data, spend money, execute code, send messages, access private data, or perform any other sensitive action.
Statuses
Every approval rule returns one of these statuses, either as a string or as an object with a type field:
| Status | Effect |
|---|---|
'not-applicable' | Execute the tool normally without approval metadata (default) |
'approved' | Record automatic approval, then execute the tool |
'denied' | Record 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 to include a reason:
toolApproval: {
deleteFile: {
type: 'denied',
reason: 'Deleting files is disabled in this workspace',
},
}Require Approval for a Tool
Use a per-tool map when each tool has a simple policy:
import { runAgentLoop } from '@fluxy-chat/agent';
const result = await runAgentLoop({
model: someModel,
tools: {
runCommand: {
inputSchema: { type: 'object', properties: { command: { type: 'string' } } },
execute: async ({ command }) => runShellCommand(command),
},
},
toolApproval: {
runCommand: 'user-approval',
},
});When runCommand is called, the agent returns approval requests 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:
toolApproval: {
processPayment: async (input, { toolCallId, messages, runtime }) => {
const amount = (input as { amount?: number }).amount ?? 0;
if (amount > 1000) {
return { type: 'user-approval' };
}
if ((runtime as { role?: string }).role !== 'admin') {
return { type: 'denied', reason: 'Only admins can send payments' };
}
return undefined; // not-applicable = execute normally
},
},One Policy for All Tools
Pass a function directly as toolApproval when approval depends on the full tool call or shared state:
toolApproval: ({ toolCall, messages, runtime }) => {
if (toolCall.name === 'deleteFile') {
return { type: 'denied', reason: 'Manual deletion disabled' };
}
if ((runtime as { env?: string }).env === 'production') {
return 'user-approval';
}
return undefined; // not-applicable
},The generic function receives:
toolCall: the full tool call (id,name,input)messages: messages sent to the model for the step that produced the callruntime: shared runtime context
Manual Approval Flow
Manual approval requires two calls when using runAgentLoop:
- Configure
toolApproval+onApprovalRequiredcallback - The callback receives an
ApprovalRequest— returntrueto approve orfalseto deny
const result = await runAgentLoop({
model: someModel,
tools: { deleteFile: { inputSchema: {}, execute: async () => 'deleted' } },
toolApproval: { deleteFile: 'user-approval' },
onApprovalRequired: async (request: ApprovalRequest) => {
const approved = await askUser(`Allow ${request.toolName}?`);
return approved;
},
maxSteps: 5,
});If approved, the tool executes. If denied, the model receives a denial result and can respond without the tool output.
HMAC-Signed Approvals
For sensitive tools that modify data, spend money, or access private resources, use toolApprovalSecret to cryptographically bind approvals to the server that issued them.
When a secret is provided, the server HMAC-signs each approval request and verifies the signature when the approval is replayed. A forged or tampered approval is rejected before the tool executes.
const result = await runAgentLoop({
model: someModel,
tools: {
deleteFile: {
inputSchema: { type: 'object', properties: { path: { type: 'string' } } },
execute: async ({ path }) => deleteFile(path),
},
},
toolApproval: { deleteFile: 'user-approval' },
toolApprovalSecret: process.env.TOOL_APPROVAL_SECRET,
onApprovalRequired: async (request) => {
// request.signature is the HMAC binding this approval to its input
return askUser(`Delete ${request.input}?`);
},
maxSteps: 5,
});Setup:
- Generate a high-entropy random string (at least 32 bytes):
openssl rand -base64 32 - Store it as an environment variable accessible to all server instances:
TOOL_APPROVAL_SECRET=your-generated-secret-here - Pass it via
toolApprovalSecret.
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
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.
Priority Resolution
resolveToolApproval follows this order:
- Generic function (if
toolApprovalis a function) — evaluated first - Per-tool function (if
toolApproval[toolName]is a function) — evaluated with typed input - Per-tool status (string or object status) — direct match
- needsApproval legacy (
tool.needsApproval) — fallback for backward compatibility
User-defined settings always take precedence over tool-defined settings.
API Reference
ToolApprovalStatus
type ToolApprovalStatus =
| undefined
| 'not-applicable'
| 'approved'
| 'denied'
| 'user-approval'
| ToolApprovalDecision;ToolApprovalDecision
interface ToolApprovalDecision {
type: 'not-applicable' | 'approved' | 'denied' | 'user-approval';
reason?: string;
}ApprovalRequest
interface ApprovalRequest {
type: 'tool-approval-request';
approvalId: string;
toolCallId: string;
toolName: string;
input: unknown;
isAutomatic?: boolean;
signature?: string;
}ApprovalResponse
interface ApprovalResponse {
type: 'tool-approval-response';
approvalId: string;
approved: boolean;
reason?: string;
providerExecuted?: boolean;
}ToolApprovalRecord
interface ToolApprovalRecord {
status: 'not-applicable' | 'approved' | 'denied' | 'user-approval';
reason?: string;
signed?: ApprovalRequest;
}ToolApprovalConfig
type ToolApprovalConfig =
| Record<string, ToolApprovalStatus | SingleToolApprovalFunction>
| GenericToolApprovalFunction;AgentLoopOptions
interface AgentLoopOptions {
// ...
toolApproval?: ToolApprovalConfig;
toolApprovalSecret?: string | Uint8Array;
onApprovalRequired?: (request: ApprovalRequest) => Promise<boolean>;
// ...
}Stream Parts
type AIStreamPart =
// ...
| { type: 'approval-request'; approvalId: string; toolCallId: string; toolName: string; input: unknown; isAutomatic?: boolean; signature?: string }
| { type: 'approval-response'; approvalId: string; toolCallId: string; toolName: string; approved: boolean; reason?: string; providerExecuted?: boolean };Legacy Support
For backward compatibility, tools can define needsApproval:
const tool: AITool = {
inputSchema: {},
needsApproval: (input) => (input as { dangerous?: boolean }).dangerous ?? false,
execute: async (input) => { /* ... */ },
};When no toolApproval config is provided, needsApproval on the tool definition is used. When both are present, toolApproval takes precedence.