Tool 승인

Tool 승인 (Tool Approvals)

기본적으로 execute 함수가 있는 tools는 모델이 호출하면 자동으로 실행돼요. ToolLoopAgent 에서 toolApproval 을 사용해 선택된 tool 호출을 실행 전에 검토, 승인, 또는 거부할 수 있어요.

toolApproval 은 데이터를 수정하거나, 돈을 쓰거나, 코드를 실행하거나, 메시지를 보내거나, 개인 데이터에 접근하거나, 기타 민감한 작업을 수행하는 tools에 유용해요.

`toolApproval` 은 AI SDK가 실행하는 tools에 적용돼요. 프로바이더가 실행하는 tools는 프로바이더 측에서 실행되며 AI SDK tool 승인을 사용하지 않아요.

출처: 문서

본문

상태 (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)

수동 승인은 두 번의 호출이 필요해요:

  1. toolApproval 과 함께 agent.generate() 또는 agent.stream() 을 호출.
  2. 결과 또는 UI 스트림에서 tool-approval-request 를 읽기.
  3. 사용자나 승인 시스템에 결정을 요청.
  4. 메시지에 tool-approval-response 를 추가.
  5. 업데이트된 메시지로 에이전트를 다시 호출.
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 결과 없이 응답할 수 있어요.

tool 실행이 거부되면 "When a tool execution is not approved, do not retry it" 같은 지시를 추가해 같은 작업에 대한 반복된 승인 요청을 방지하는 것을 고려하세요.

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, 입력 인자에 바인딩해요. 서명 후 이 중 하나라도 바뀌면 승인이 무효화돼요.

시크릿 설정하기:

  1. 높은 엔트로피 무작위 문자열(최소 32바이트) 생성:
    openssl rand -base64 32
    
  2. 모든 서버 인스턴스가 접근할 수 있는 환경 변수로 저장:
    TOOL_APPROVAL_SECRET=your-generated-secret-here
    
  3. experimental_toolApprovalSecret 을 통해 ToolLoopAgent, generateText, streamText 에 전달.

요청을 처리할 수 있는 모든 serverless 인스턴스는 같은 시크릿이 필요해요. 한 인스턴스가 승인을 서명하고 다음 턴에 다른 인스턴스가 검증할 수 있기 때문이에요.

구성 시 동작:

  • 유효한 서명이 없는 승인 요청은 거부됨(fail-closed)
  • 시크릿 미설정: 승인은 이전처럼 동작(백워드 호환)
  • 시크릿은 절대 클라이언트로 보내지거나 스트림에 포함되지 않음
`WorkflowAgent` 도 `experimental_toolApprovalSecret` 을 지원해요. 내구성 있는 승인 요청을 쓰기 전에 워크플로 단계에서 서명해요. `{ environmentVariable: 'TOOL_APPROVAL_SECRET' }` 과 같은 환경 변수 참조를 전달해 원시 시크릿이 서명·검증 단계 안에서만 읽히게 하세요. 서명만 지속되고 클라이언트로 보내져요.

관련 API

  • toolApproval 을 ToolLoopAgent, generateText, streamText 와 함께 사용.
  • 승인 규칙을 코드로 작성하려면 Policy-Based Tool Approvals (@ai-sdk/policy-opa) 참조.
  • needsApproval 은 WorkflowAgent 에서만 사용. 여기서 승인은 내구성 있는 워크플로 실행을 일시 중단하고 재개해요.
  • 서브에이전트 tools는 toolApproval 을 사용할 수 없어요. Subagents 참조.

더 알아보기 (Learn more)

전체 사이트맵