Tool 승인
Tool 승인 (Tool Approvals)
기본적으로 execute 함수가 있는 tools는 모델이 호출하면 자동으로 실행돼요. ToolLoopAgent 에서 toolApproval 을 사용해 선택된 tool 호출을 실행 전에 검토, 승인, 또는 거부할 수 있어요.
toolApproval 은 데이터를 수정하거나, 돈을 쓰거나, 코드를 실행하거나, 메시지를 보내거나, 개인 데이터에 접근하거나, 기타 민감한 작업을 수행하는 tools에 유용해요.
출처: 문서
본문
상태 (Statuses)
모든 승인 규칙은 문자열 또는 type 필드를 가진 객체로 다음 상태 중 하나를 반환해요:
'not-applicable': 승인 메타데이터 없이 tool을 정상적으로 실행. 기본값.'approved': 자동 승인을 기록한 후 tool 실행.'denied': 자동 거부를 기록하고 거부된 tool 출력을 반환.'user-approval': 승인 요청을 내보내고 명시적 응답을 기다림.
자동 승인과 거부에서 이유(reason)를 포함하고 싶을 때는 객체 형태를 사용하세요:
toolApproval: {
deleteFile: {
type: 'denied',
reason: 'Deleting files is disabled in this workspace',
},
}
승인 함수는 undefined 를 반환할 수도 있는데, 이는 'not-applicable' 과 동일하게 취급돼요.
Tool에 대한 승인 요구 (Require Approval for a Tool)
각 tool이 단순한 정책을 가질 때는 tool별 맵을 사용하세요.
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',
},
});
runCommand 가 호출되면 에이전트는 tool을 실행하는 대신 tool-approval-request 를 반환해요.
Tool 입력에 기반해 결정하기 (Decide Based on Tool Input)
결정이 파싱된 tool 입력에 의존할 때는 tool별 승인 함수를 사용하세요. 이 함수는 타입화된 입력과 toolCallId, messages, toolContext, runtimeContext 를 받아요.
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;
},
},
});
이 예제에서 비관리자 사용자는 자동으로 거부되고, 큰 결제는 수동 승인이 필요하며, 더 작은 관리자 결제는 정상적으로 실행돼요.
모든 Tool에 하나의 정책 사용하기 (Use One Policy for All Tools)
승인이 전체 tool 호출, tool 간 공유 상태, 또는 전체 tool 세트에 의존할 때는 함수를 toolApproval 로 직접 전달하세요. 이를 GenericToolApprovalFunction 이라고 해요.
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;
},
});
제네릭 함수는 다음을 받아요:
toolCall:toolName,toolCallId,input, 동적인지 여부를 포함한 전체 tool 호출.tools: 모델이 사용할 수 있는 모든 tools.toolsContext: 모든 tools에 대한 컨텍스트.messages: tool 호출을 만든 단계에서 모델로 보낸 메시지.runtimeContext: 호출의 공유 런타임 컨텍스트.
요청별로 승인 설정하기 (Configure Approval per Request)
toolApproval 은 에이전트 설정이므로 prepareCall 에서 반환할 수도 있어요. 승인 정책이 호출 옵션, 테넌트 정책, 사용자 권한에 의존할 때 유용해요.
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)
수동 승인은 두 번의 호출이 필요해요:
toolApproval과 함께agent.generate()또는agent.stream()을 호출.- 결과 또는 UI 스트림에서
tool-approval-request를 읽기. - 사용자나 승인 시스템에 결정을 요청.
- 메시지에
tool-approval-response를 추가. - 업데이트된 메시지로 에이전트를 다시 호출.
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 });
승인되면 tool은 두 번째 호출에서 실행돼요. 거부되면 모델은 거부를 받고 tool 결과 없이 응답할 수 있어요.
useChat 와 함께 사용하기 (Use with useChat)
에이전트를 채팅 UI로 스트리밍할 때 승인 요청은 state: 'approval-requested' 를 가진 tool 파트로 나타나요. addToolApprovalResponse 로 응답하세요.
'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}>
{part.approval.requestReason && (
<p>{part.approval.requestReason}</p>
)}
<button
onClick={() =>
addToolApprovalResponse({
id: part.approval.id,
approved: true,
})
}
>
Approve
</button>
<button
onClick={() =>
addToolApprovalResponse({
id: part.approval.id,
approved: false,
})
}
>
Deny
</button>
</div>
);
}
}),
);
}
addToolApprovalResponse 는 수동 승인에만 호출하세요. 자동 승인과 거부는 이미 스트림에 승인 상태를 포함해요. 수동 승인 상태에 reason이 포함되면 part.approval.requestReason 으로 사용할 수 있어요. addToolApprovalResponse 로 제공된 reason은 part.approval.reason 으로 별도 저장돼요.
스키마 변환과 지속된 승인 (Schema transforms and persisted approvals)
승인 요청은 승인에 제시된 입력과 다를 때 원래 스키마 입력을 inputSchemaInput 에 보존해요. responseMessages 나 UI 메시지를 지속할 때 이 필드를 유지하세요. UI 메시지 변환 함수는 자동으로 보존해요.
연속 시 SDK는 변환된 입력을 재구성하고 승인된 입력과 일치하는지 확인해요. 승인된 입력을 다른 값으로 절대 대체하지 않아요. 원래 입력이 생략된 오래된/투영된 기록은 재검증이 실패하거나 승인된 값을 바꾸면 유효하지 않은 tool 입력으로 거부돼요.
원래 입력은 스키마 변환이 제거한 필드를 포함해 지속된 메시지와 UI 승인 스트림에 포함돼요. 필드를 제거하는 변환은 승인 메타데이터에서 그 필드를 redact하지 않아요.
보안 고려 사항 (Security Considerations)
신뢰 모델 (Trust model)
표준 useChat 패턴에서 서버는 클라이언트가 매턴 보내는 메시지로 대화를 재구축해요. 서버는 요청 사이에 대화 상태를 지속하지 않아요. 즉 메시지 기록은 클라이언트가 제어하는 입력이에요.
이 기록에서 재구성된 tool 승인은 실행 전에 재검증돼요. tool 입력은 tool의 스키마와 대조되고 승인 정책은 재평가돼요. 그러나 추가 보호가 없다면, 스키마에 부합하는 입력에 대해 유효해 보이는 승인을 조작하는 클라이언트는 human-in-the-loop 단계를 우회할 수 있어요.
여러분의 tools가 민감한 작업(데이터 수정, 지출, 외부 API 호출, 개인 리소스 접근)을 수행한다면 experimental_toolApprovalSecret 을 사용해 승인을 그것을 발급한 서버에 암호학적으로 바인딩하세요.
experimental_toolApprovalSecret 로 승인 서명하기 (Signing approvals with experimental_toolApprovalSecret)
시크릿을 제공하면 서버는 발급 시 각 승인 요청을 HMAC 서명하고, 승인이 다시 재생될 때 서명을 검증해요. 위조되거나 변조된 승인은 tool이 실행되기 전에 거부돼요. ToolLoopAgent 에 시크릿을 설정하거나(generateText 나 streamText 에 직접 전달) 하세요.
const agent = new ToolLoopAgent({
model: __MODEL__,
tools: { deleteFile, runQuery },
toolApproval: { deleteFile: 'user-approval', runQuery: 'user-approval' },
experimental_toolApprovalSecret: process.env.TOOL_APPROVAL_SECRET,
});
const result = await agent.generate({
messages,
});
서명은 승인을 정확한 tool 이름, tool 호출 ID, 입력 인자에 바인딩해요. 서명 후 이 중 하나라도 바뀌면 승인이 무효화돼요.
시크릿 설정하기:
- 높은 엔트로피 무작위 문자열(최소 32바이트) 생성:
openssl rand -base64 32 - 모든 서버 인스턴스가 접근할 수 있는 환경 변수로 저장:
TOOL_APPROVAL_SECRET=your-generated-secret-here experimental_toolApprovalSecret을 통해ToolLoopAgent,generateText,streamText에 전달.
요청을 처리할 수 있는 모든 serverless 인스턴스는 같은 시크릿이 필요해요. 한 인스턴스가 승인을 서명하고 다음 턴에 다른 인스턴스가 검증할 수 있기 때문이에요.
구성 시 동작:
- 유효한 서명이 없는 승인 요청은 거부됨(fail-closed)
- 시크릿 미설정: 승인은 이전처럼 동작(백워드 호환)
- 시크릿은 절대 클라이언트로 보내지거나 스트림에 포함되지 않음
관련 API
toolApproval을ToolLoopAgent,generateText,streamText와 함께 사용.- 승인 규칙을 코드로 작성하려면 Policy-Based Tool Approvals (
@ai-sdk/policy-opa) 참조. needsApproval은WorkflowAgent에서만 사용. 여기서 승인은 내구성 있는 워크플로 실행을 일시 중단하고 재개해요.- 서브에이전트 tools는
toolApproval을 사용할 수 없어요. Subagents 참조.