정책 기반 Tool 승인
정책 기반 Tool 승인 (Policy-Based Tool Approvals)
Tool Approvals 를 사용하면 에이전트 설정의 함수로 tool 호출을 승인하거나 거부할 수 있어요. @ai-sdk/policy-opa 는 그 규칙들을 코드 밖으로 옮겨 Open Policy Agent(OPA)의 .rego 정책으로 만들 수 있게 해줘요.
다음 같은 경우에 쓰면 좋아요:
- 인가(authorization)를 에이전트 설정에 묻힌 함수가 아니라 별도의, 검토 가능한 산출물로 작성하고 싶을 때.
- SDK와 독립적으로 CI에서
opa test로 테스트 가능하게 하고 싶을 때. - 코드 배포 없이 편집하고 싶을 때(실행 중인 OPA 인스턴스에서 서비스될 때).
이 패키지는 전적으로 공개 toolApproval 콜백 위에 얹혀 있어요. 통신 내용은 바뀌지 않아요. 내장 승인과 동일한 tool-approval-request / tool-approval-response 흐름을 그대로 사용해요.
출처: 문서
본문
무엇을 강제할 수 있나 (What you can enforce)
OPA는 일반 정책 엔진이에요. 구조화된 입력에 대한 코드로 규칙을 표현할 수 있다면 tool 경계에서 강제할 수 있어요. 흔한 범주:
- 보안 및 접근: 스코프, 역할, 권한, 테넌트 격리, tool·호스트·경로의 허용 목록.
- 비즈니스 규칙: 누가 어떤 리소스에서 어떤 조건으로 무엇을 할 수 있는지(예: 임계값을 넘는 결제는 승인이 필요).
- 비용 및 사용량: 예산, 토큰 상한, 사용자별 할당량을 초과하는 호출을 거부하거나 승인 요구.
- 규정 준수 및 변경 통제: 파괴적이거나 규제되는 작업을 올바른 승인자, 환경, 시간 창에 게이트.
결정은 현재 호출의 인자에만 국한되지 않아요. 정책 input 은 messages(실행의 전체 모델·tool 호출 기록)도 담아서, 이미 일어난 일(이전 tool 호출, 지금까지의 일련의 작업, 대화 전체의 누적 합계)을 규칙에 반영할 수 있어요. 예:
- 실행이 이미 N번 쓰기(write)를 수행하면 승인 요구.
- 같은 대화에서 두 번째 돌이킬 수 없는 작업(push, delete) 거부.
- 단계별 누적 지출이나 토큰 사용량을 추적해 상한에서 멈추기.
가장 잘 맞는 경우: 결정적 검사 (Best fit: deterministic checks)
결정이 구조화된 필드(현재 호출이든 messages 의 기록이든)에서 결정적이고 검증 가능할 때 정책 강제가 가장 강력해요. 정책이 값을 추출해 비교하므로 매번 같은 답이 나와요.
- 잘 맞는 경우: 스코프, 권한, 숫자 임계값, 허용 목록, 시간 창, 그리고 개수·순서·누적 합계 같은 기록 인지 검사.
- 약한 맞춤: "나쁜 말 쓰지 마", "유해 콘텐츠 차단" 같은 콘텐츠 기반·의미 기반 필터링. 이들은 best-effort이고 검증하기 어렵고 우회하기 쉬워요. 전용 moderation/classification 단계를 쓰고, 그 주변의 결정적 게이트에는 정책을 유지하세요.
경험 법칙: input 객체의 어떤 필드를 가리키고 그 필드에 정확한 검사를 쓸 수 있다면 이 도구가 맞아요. 규칙이 자유 형식 의미 판단에 의존한다면 정책만으로는 부족하니 다른 수단을 쓰세요.
설치 (Install)
pnpm add @ai-sdk/policy-opa
# pick one (or both) OPA backends:
pnpm add @open-policy-agent/opa-wasm # in-process WASM evaluation
pnpm add @open-policy-agent/opa # HTTP client to a running OPA server
- 백엔드는 선택적 peer 의존성이에요.
- import 한 백엔드만 로드돼요.
동작 원리 (How it works)
정책은 매 tool 디스패치 전에 조회되고 표준 승인 상태 중 하나로 매핑돼요:
allow는 tool을 실행해요.deny는 모델이 추론할 수 있는 거부 결과를 반환해요(사람 필요 없음).requires-approval은 실행을 잠시 멈추고 사람의tool-approval-response를 기다려요.- 일치하는 규칙이 없으면
not-applicable로 정규화되며 SDK는 이를 allow로 취급해요. 기본 거부로 하려면 정책에default decision := { "decision": "deny" }를 추가하세요.
빠른 시작 (Quick start)
import { generateText } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
import { opaPolicy, wasmPolicyClient } from '@ai-sdk/policy-opa';
import { readFile } from 'node:fs/promises';
// 1. Load the compiled policy bundle.
const wasm = await readFile('./policy.wasm');
const client = await wasmPolicyClient({ wasm });
// 2. Build the toolApproval configuration.
const toolApproval = opaPolicy({
client,
path: 'agent/call/decision',
});
// 3. Pass it to generateText (or streamText / ToolLoopAgent). Everything else is normal.
const result = await generateText({
model: anthropic('claude-sonnet-5'),
tools: { git, bash, queryLogs },
toolApproval,
prompt: 'find the failing test and push the fix',
});
Rego 정책 작성하기 (Writing the Rego policy)
정책은 결정 객체를 내보내요. reason 은 선택사항이며 모델(deny 의 경우) 또는 사람 승인자(requires-approval 의 경우)에게 표시돼요. 수동 승인 요청은 핵심 결과에서 reason 으로, UI 메시지에서 approval.requestReason 으로 노출돼요.
package agent.call
# Default to "not-applicable" so unmatched calls fall through.
# Use { decision: "deny" } to default-deny instead.
default decision := { "decision": "not-applicable" }
# Hard deny: pushes are never allowed automatically.
decision := { "decision": "deny", "reason": "pushes require human review" } {
input.tool.name == "git"
input.args.args[0] == "push"
}
# Auto-allow: read-only git operations.
decision := { "decision": "allow" } {
input.tool.name == "git"
input.args.args[0] in {"status", "log", "diff", "show"}
}
# Human-in-the-loop: kubectl by oncall.
decision := { "decision": "requires-approval", "reason": "kubectl by oncall" } {
input.tool.name == "kubectl"
input.runtimeContext.role == "sre-oncall"
}
참고:
- 레거시 부울 형태(
{ "allow": true | false, "reason": "..." })도 허용되므로 기존 규칙을 재작성 없이 마이그레이션할 수 있어요. - 기본 OPA
input형태는{ tool: { name }, args, messages, runtimeContext }이에요. toInput으로 input 형태를 오버라이드하세요:
opaPolicy({
client,
path: 'agent/call/decision',
toInput: ({ toolCall, runtimeContext }) => ({
action: toolCall.toolName,
principal: runtimeContext.role,
resource: toolCall.input,
}),
});
CI에서 정책 테스트하기 (Test the policy in CI)
OPA는 자체 테스트 프레임워크를 제공해요. 이 테스트는 SDK 없이 실행되며, 이것이 policy-as-code가 앱 코드의 정책보다 나은 주된 이유예요.
# policy_test.rego
package agent.call
test_push_denied {
decision.decision == "deny" with input as {
"tool": { "name": "git" },
"args": { "args": ["push", "origin", "main"] }
}
}
opa test policy.rego policy_test.rego 로 실행하세요.
오류는 폐쇄 실패(fail closed)로 처리
- 백엔드가 오류면(서버 연결 불가, WASM 결함, 잘못 빌드된 번들)
opaPolicy는 오류 메시지를 reason으로 한denied를 반환해요. - 백엔드가 있지만 인식할 수 없는 결정(알 수 없는
decision값, 비부울 레거시allow값)을 반환하면opaPolicy는denied를 반환해요. - 오류는 콜백 밖으로 reject되지 않고 실행을 중단하지 않아요.
- 백엔드 중단은 조용히 허용하는 대신 해당 호출을 차단해요.
- 이는 일치하지 않는 규칙과는 달라요.
null이나undefined는not-applicable(허용)로 정규화돼요. 일치하지 않는 호출도 거부하려면default ... deny규칙을 사용하세요.
정책 로드하기 (Loading the policy)
옵션 A: WASM (in-process)
.rego 를 미리 WASM으로 컴파일하세요:
opa build -t wasm -e 'agent/call/decision' -o bundle.tar.gz policy.rego
tar -xzf bundle.tar.gz /policy.wasm
import { wasmPolicyClient, opaPolicy } from '@ai-sdk/policy-opa';
import { readFile } from 'node:fs/promises';
const wasm = await readFile('./policy.wasm');
const client = await wasmPolicyClient({ wasm });
const toolApproval = opaPolicy({ client, path: 'agent/call/decision' });
- 결정당 네트워크 호출이 없어요.
- 정책을 앱과 함께 배포하거나 시작 시 객체 스토리지에서 가져올 때 잘 맞아요.
- 핫 리로드는 WASM을 재빌드하고 클라이언트를 다시 인스턴스화하는 것을 의미해요.
옵션 B: HTTP (실행 중인 OPA 서버)
import { httpPolicyClient, opaPolicy } from '@ai-sdk/policy-opa';
const client = httpPolicyClient({ url: 'http://localhost:8181' });
const toolApproval = opaPolicy({ client, path: 'agent/call/decision' });
- 결정당 HTTP 라운드트립이 한 번 있어요.
- 정책이 자주 바뀌고 재배포 없이 핫 리로드를 원할 때, 또는 여러 서비스가 하나의 OPA를 공유할 때 잘 맞아요.
- Styra DAS / EOPA 인증을 위해
headers를 전달하세요.
외부 데이터와 통합 가져오기 (Bring in external data and integrations)
정책은 tool 호출 입력에만 국한되지 않아요. OPA는 역할-권한 매핑, IDP 그룹 멤버십, 자격 목록, 허용 목록 같은 외부 데이터를 기반으로 결정할 수 있고, 그 데이터는 앱을 재배포하지 않고 업데이트할 수 있어요.
OPA가 데이터를 어떻게 소싱하는지는 이 패키지의 문제가 아니라 OPA의 문제예요. 일반적인 접근:
- 로드 시 정적 데이터:
wasmPolicyClient({ wasm, data })에data를 전달하세요. 이는 번들의setData에 전달되므로 정책이data.*아래에서 읽을 수 있어요. - 번들: 실행 중인 OPA 서버(HTTP 백엔드)를 번들 서비스에 가리켜 주기적으로 새 정책과 데이터를 풀링하게 하세요. 역할 매핑과 IDP 그룹은 코드 배포 없이 업데이트돼요.
- 평가 중 조회: 정책이 결정을 평가하는 동안 외부 서비스(예: IDP 또는 자격 API)를 호출할 수 있어요.
자세한 내용은 여기 말고 OPA에서 배우세요:
- Policy reference
- External data (IDP 연결 포함)
이 패키지는 결과 결정을 SDK의 승인 상태에 매핑하기만 해요. 전달하는 input 은 정책이 OPA가 이미 가진 데이터와 결합하는 대상이에요.
섀도 모드로 안전하게 배포하기 (Roll out safely with shadow mode)
새 정책을 바로 enforce로 배포하지 마세요. 첫 버전은 거의 항상 의도하지 않은 것을 거부해요. shadow(approval, opts) 는 정책을 평가하고 onDecision 으로 결정을 보고하지만, enforce: true 로 전환하기 전까지는 모든 호출을 승인했다고 SDK에 알려줘요.
import { opaPolicy, shadow, wasmPolicyClient } from '@ai-sdk/policy-opa';
const client = await wasmPolicyClient({ wasm });
const toolApproval = shadow(
opaPolicy({ client, path: 'agent/call/decision' }),
{
enforce: process.env.ENFORCE_POLICY === 'true',
onDecision: event => {
logger.info('policy.decision', {
tool: event.toolCall.toolName,
decision: event.decision.type,
reason: event.decision.reason,
enforced: event.enforced,
wouldBlock: event.decision.type === 'denied',
});
},
},
);
권장 배포 절차:
- 정책을 작성하고
opa test로 테스트하세요. enforce: false(기본값)로shadow(...)에 감싸고onDecision을 로그/메트릭에 연결하세요.- 실제 환경에서 실행하세요.
decision.type이denied또는user-approval인 이벤트를 검사하세요. 이것들이 정책이 바꿨을 호출들이에요. - 정책을 고치고 반복하세요.
denied/user-approval이벤트가 원하는 것뿐일 때enforce: true를 설정하세요.
텔레메트리 의미론:
onDecision은 fire-and-forget이에요. 느리거나 throw하는 로거는 강제를 막거나 깨뜨릴 수 없고, throw된 오류는 삼켜져요.- 계약상 강제가 우선이고 관측성은 그다음이에요.
- 반대(승인이 감사 로그를 기다림)가 필요하면
shadow를 통하지 말고 기본toolApproval안에서 로그하세요.
결정을 관측성 플랫폼으로 보내기 (Send decisions to your observability platform)
onDecision 은 배포 전용이 아니에요. enforce: true 로 shadow 를 유지하면 정책이 실부하를 담당하는 동안 모든 결정이 로깅, 메트릭, 트레이싱 스택으로 흘러가요.
const toolApproval = shadow(
opaPolicy({ client, path: 'agent/call/decision' }),
{
enforce: true, // enforcing AND observing
onDecision: event => {
metrics.increment('agent.policy.decision', {
tool: event.toolCall.toolName,
decision: event.decision.type, // approved | denied | user-approval | not-applicable
enforced: String(event.enforced),
});
},
},
);
- 각
PolicyDecisionEvent는 tool 호출, 정책decision(유형과 이유),enforced,effective(SDK가 실제로 처리한 것)를 담아요.decision과effective를 비교해 드리프트를 발견하세요. onDecision은 fire-and-forget이에요. 섀도 모드의 텔레메트리 의미론을 참고하세요.
HTTP 백엔드를 사용하면 OPA는 결정 로그 를 네이티브로 내보낼 수도 있어요. 애플리케이션 코드 없이 모든 평가를 원격 서비스로 보내 완전한 감사 추적을 만들 수 있어요.
모델 경계에서의 기능 스코핑 (Capability scoping at the model boundary)
opaCapabilityMiddleware 는 toolApproval 보다 더 일찍, 즉 모델에게 tool이 존재한다는 사실조차 알리기 전에 정책을 강제해요. 이는 심층 방어(defense in depth)이면서도 토큰을 절약하고 탈옥(jailbreak) 거부를 개선해요.
import { wrapLanguageModel } from 'ai';
import { wasmPolicyClient, opaCapabilityMiddleware } from '@ai-sdk/policy-opa';
const client = await wasmPolicyClient({ wasm });
const wrappedModel = wrapLanguageModel({
model: anthropic('claude-sonnet-5'),
middleware: opaCapabilityMiddleware({ client, path: 'agent/tools/allowed' }),
});
path의 규칙은 허용된 tool 이름의string[]또는{ tools: string[] }를 반환해요.- 허용 목록에 없는 tools는 모델이 보기 전에 제거돼요.
- 폐쇄 실패: 잘못된 응답이나 평가자 오류 시
params.tools가undefined로 설정되어 모델에게 tool이 없다고 알려요. 개방 실패(fail-open)가 필요하면 Rego에 폴백을 작성하세요.
발견된 tool 표면 스코핑하기 (Scoping a discovered tool surface)
tools가 MCP 발견이나 플러그인 레지스트리에서 올 때는 tool별 규칙을 미리 작성할 수 없고, 잊은 tool은 조용히 허용돼요. wrapMcpTools 는 일치하지 않는 tools를 설정 가능한 기본값으로 라우팅해 발견된 표면에 대해 승인을 전체(total)로 만들어요.
import { opaPolicy, wasmPolicyClient, wrapMcpTools } from '@ai-sdk/policy-opa';
const discovered = await mcpClient.tools();
const client = await wasmPolicyClient({ wasm });
const { tools, toolApproval } = wrapMcpTools(
discovered,
opaPolicy({ client, path: 'agent/call/decision' }),
{ default: 'user-approval' }, // anything OPA does not match needs a human
);
await generateText({ model, tools, toolApproval, prompt });
발견되지 않은 tool의 기본값:
'user-approval'(기본값): 사람 필요. 소스를 신뢰하지만 안전망을 원할 때 좋아요.'denied': 엄격한 허용 목록 모드. 정책이 허용되는 것을 열거하고 나머지는 거부돼요.'approved': 허용. 발견 소스가 완전히 신뢰될 때만.
이름과 달리 어떤 Record<string, Tool> 에서도 동작해요.
정책이 설정되지 않았을 때 모두 허용 (Allow-all when no policy is configured)
optionalOpaPolicy 는 client 가 undefined 일 때 undefined 를 반환하는데, 이는 toolApproval 을 전달하지 않은 것과 같아요(SDK가 모든 호출을 승인). 정책 파일이 없는 로컬 개발이나 CI에 유용해요.
import { optionalOpaPolicy, wasmPolicyClient } from '@ai-sdk/policy-opa';
import { readFile } from 'node:fs/promises';
const wasm = process.env.POLICY_WASM_PATH
? await readFile(process.env.POLICY_WASM_PATH)
: undefined;
const client = wasm ? await wasmPolicyClient({ wasm }) : undefined;
const toolApproval = optionalOpaPolicy({ client, path: 'agent/call/decision' });
POLICY_WASM_PATH미설정:toolApproval은undefined, 모든 호출 허용, OPA 모듈은 로드되지 않음.POLICY_WASM_PATH설정: 정책 로드, 강제 켜짐.- 더 엄격한 동작(정책 없이 시작 거부)을 원하면
opaPolicy를 직접 사용하고 시작 시 누락된 바이트가 throw하게 하세요.
전이 강제: 복합 tool (Transitive enforcement: composite tools)
toolApproval 은 모델이 tool을 직접 호출할 때만 발동해요. 거친 디스패처 tool(git push 를 실행할 수 있는 bash tool, HTTP tool, MCP 프록시)은 모델로 하여금 그 tool을 통해 라우팅함으로써 작업별 규칙을 우회하게 할 수 있어요.
해결책은 디스패처의 toolApproval 항목 안에 있어요. 디스패처 입력을 논리적 (name, args) 쌍으로 파싱한 다음, 직접 tool이 쓰는 동일한 규칙으로 평가하세요.
const bashApproval = opaPolicy({
client,
path: 'agent/action/decision',
toInput: ({ toolCall }) => {
const { command } = toolCall.input as { command: string };
const [bin, ...rest] = command.split(/\s+/);
return { kind: bin, args: rest };
},
});
지침:
- 일치 로직을 한곳에 유지하세요. 공유 Rego 헬퍼 규칙(두 승인이 같은
path로opaPolicy호출) 또는 공유 TypeScript 술어. - 깨끗한 호출로 줄일 수 없는 것은 모두 거부하세요. 셸 입력은 파싱하기 적대적이므로 "안전함을 증명할 수 없다"는 것은 거부를 의미해요.
- 정직한 한계: 이것은 모델의 호출 경계에서 디스패치를 게이트해요. 일단 승인된 tool이 그 입력이 설명한 것 이상으로 추가 부수효과를 수행하는 것을 막지는 못해요. 그런 경우 신뢰할 수 없는 실행을 대역외 sandbox(Vercel Sandbox, Firecracker, 컨테이너)에서 실행하고 sandbox를 신뢰 경계로 취급하세요.
SQL, HTTP, MCP, 브라우저, 셸 디스패처를 위한 작업 예제는 패키지 README 에 있어요.
예제 애플리케이션 (Example application)
ai-sdk-slackbot 예제는 이 패키지를 실제 에이전트에 end-to-end로 연결해요. 다음을 보여줘요:
- 모든 tool 호출이
policies/decision.rego의 Rego로 게이트되며 TypeScript를 건드리지 않고 편집 가능. - 정책 아래의 실제 tools: 도메인에 스코프된 웹 검색, 도시 허용 목록을 가진 날씨 tool, 읽기 전용 명령 허용 목록으로 제한된
bashtool(전이 강제). - 하나의 환경 변수(
POLICY_MODE)로 백엔드 전환: 기본은 in-process WASM, 또는 dev에서 정책 핫 리로딩을 위한 라이브 OPA HTTP 서버. - 정책 코드가
policies/decision_test.rego의 OPA 네이티브 테스트로 독립적으로 테스트됨.
정책이 로드되고 적용되는 방식은 policies/ 와 lib/policy/load.ts 를 보세요.
API 참조 (API reference)
모든 것은 패키지 루트 @ai-sdk/policy-opa 에서 내보내져요.
엔진 중립 핵심:
shadow(approval, opts?): 어떤 승인이든opts.enforce: true까지는 평가되고 보고되지만 강제되지 않도록 감싸요. 새 정책의 권장 시작점.wrapMcpTools(tools, approval, opts?): 발견된 tool 세트에 대해 승인을 전체로 만들어요.opts.default가 발견되지 않은 tool을 제어해요.PolicyClient: 모든 백엔드가 구현하는evaluate(path, input)인터페이스. 비-OPA 엔진을 위한 접합부(seam).- 헬퍼 타입:
PolicyDecision,WrappedMcpTools,PolicyDecisionEvent.
OPA 백엔드 및 어댑터:
wasmPolicyClient({ wasm, data? }): 비동기. 컴파일된 OPA WASM 번들을 in-process로 로드.httpPolicyClient({ url, headers? }): 동기. 실행 중인 OPA 서버에 대한 클라이언트.opaPolicy({ client, path, toInput? }):toolApproval설정을 반환. 폐쇄 실패.optionalOpaPolicy({ client, path, toInput? }):opaPolicy와 같지만client가undefined면undefined를 반환.opaCapabilityMiddleware({ client, path, toInput? }):params.tools를 허용 목록으로 좁히는LanguageModelV4Middleware. 폐쇄 실패.normalizeOpaDecision(result): OPA를 직접 호출하는 사용자를 위한 독립 결과 정규화.
관련
- Tool Approvals: 이 패키지가 연결되는 기본
toolApproval콜백. - Building Agents
- ai-sdk-slackbot example: 정책 게이트된 tools를 가진 완전한 에이전트.
- Open Policy Agent documentation