Ch7. Tools, Approval, Connections
Eve 도구 실행, 사람 승인, MCP/OpenAPI 연결, OAuth 흐름을 엔터프라이즈 보안 경계로 설계한다.
핵심 요약
- authored tool의
execute는 sandbox가 아니라 app runtime에서 돌아갑니다. privileged surface로 다뤄야 합니다. toModelOutput,approval, auth scope는 tool이 어떤 데이터를 노출하고 어떤 위험 행동을 하는지 막는 핵심 장치입니다.- MCP/OpenAPI connection은 확장이 빠른 만큼 allowlist, approval, OAuth 책임 경계를 함께 설계해야 합니다.
0.78–0.82 승인 주체와 실행 주체
현재 defineTool은 approval을 씁니다. 승인된 호출은 요청자의 권한으로 실행됩니다. 승인자의 권한이 자동으로 도구 실행에 넘어오지 않습니다. 승인 후 workflow/agent dispatch 전에 요청자 권한도 재확인합니다.
approval: { request: always(), response: ... }에서 response 정책을 지정하면 기본 요청자 전용 정책을 대체합니다. principalId만 비교하지 말고 authenticator·principalType·tenant 등 전체 신원을 확인합니다. 서버의 approval.settled 또는 input.resolved가 최종 판정이며, 응답 제출이나 approval.candidate 수신만으로 실행 승인으로 표시하지 않습니다.
defineTool 실행 중 선택적으로 ctx.approval.responder를 조회할 수 있지만, 승인자 토큰을 쓰려면 명시적인 별도 설계가 필요합니다. auto()는 모델 기반 승인 정책이며 데이터가 판정 모델로 전송될 수 있으므로 인증·인가 검사 대신 쓰지 않습니다. once()의 이미 대기 중인 여러 요청도 각각 해소해야 합니다.
도구 검색과 실제 이름
0.82.0은 eve__search로 검색하고 eve__tool({ name, input })로 지연 도구·연결 도구를 호출합니다. 연결 도구 이름은 <connection>__<tool>입니다. skills는 eve__skill({ name })로 읽습니다. trace와 eval에는 catalog wrapper가 아니라 실제 호출한 도구/skill이 기록됩니다.
Eve에서 tool은 모델이 호출하는 typed action입니다. 한 가지만 기억하면 됩니다. authored tool의 execute는 sandbox가 아니라 app runtime에서 돌아갑니다. 그래서 tool은 비밀값과 내부 서비스를 건드릴 수 있는 privileged surface입니다.
Tool 기본 구조
import { defineTool } from "eve/tools";
import { z } from "zod";
export default defineTool({
description: "Return mock weather data for a city.",
inputSchema: z.object({ city: z.string().min(1) }),
async execute({ city }, ctx) {
return { city, condition: "Sunny", temperatureF: 72 };
},
});운영 기준:
description은 모델이 언제 호출할지 판단하는 문장이다.inputSchema는 방어선이다. 빈 입력도z.object({})로 명시한다.execute는 재시도와 재실행을 염두에 두고 짠다.- 반환값은 모델 history에 남으니 최소화한다.
toModelOutput으로 모델 가시성 줄이기
channel/hook에는 rich output을 주되 모델에는 요약만 보여야 할 때 toModelOutput을 씁니다.
// Spread this projection into defineTool({ ...outputProjection, ... }).
const outputProjection = {
toModelOutput(output: { id: string; riskLevel: string }) {
return { type: "text" as const, value: `Report ${output.id}: risk ${output.riskLevel}.` };
},
};엔터프라이즈에서는 이 패턴이 사실상 필수입니다. CRM row, audit payload, pricing table, PII를 모델에 그대로 돌려주면 후속 응답과 telemetry에 그 값이 섞여 들어갑니다.
승인 정책
Eve는 tool 단위로 approval을 둡니다.
| Helper | 동작 |
|---|---|
never() | 승인 불필요. 생략 시 기본값 |
once() | 세션에서 첫 실행만 승인 |
always() | 매 실행 승인 |
| predicate | 입력값에 따라 승인 여부 결정 |
민감한 side effect는 생략 기본값에 기대면 안 됩니다.
import { defineTool } from "eve/tools";
import { always } from "eve/tools/approval";
import { z } from "zod";
export default defineTool({
description: "Refund a customer charge.",
inputSchema: z.object({
chargeId: z.string(),
amount: z.number().positive(),
}),
approval: always(),
async execute(input) {
return refundCharge(input);
},
});입력 기반 approval
금액, 대상, 계정 tier에 따라 승인 기준을 바꿀 수 있습니다.
const approval = ({ toolInput }: { toolInput?: { amount?: number } }) => {
const amount = Number(toolInput?.amount ?? 0);
return amount > 1000;
};실무에서는 predicate 안에 비즈니스 로직을 욱여넣지 말고 agent/lib/policy.ts에 순수 함수로 떼어 unit test를 붙입니다.
HITL은 durable park다
승인이 필요하면 Eve는 input.requested event를 내고 session을 turn.waiting로 park합니다. compute를 붙잡고 기다리는 게 아니라 workflow가 durably suspend됩니다. 사용자가 승인하거나 거절하면 같은 session이 다시 살아납니다.
장기 백오피스 작업에서 이 특성이 빛을 봅니다.
| 상황 | Eve 동작 |
|---|---|
| 담당자가 3시간 뒤 승인 | session이 이어짐 |
| 프로세스 재시작 | 마지막 checkpoint에서 재개 |
| 승인 중 새 메시지 도착 | approval resolution과 message가 분리 처리될 수 있음 |
| tool step 재실행 | approval 기록이 state에 남아 중복 prompt를 피함 |
Connections: 외부 tool provider
Connections는 agent가 직접 짜지 않은 외부 MCP/OpenAPI tool을 모델에 노출합니다.
| 연결 | 정의 helper | 모델-visible tool name |
|---|---|---|
| MCP | defineMcpClientConnection | <connection>__<tool> |
| OpenAPI | defineOpenAPIConnection | <connection>__<operationId> |
모델은 먼저 eve__search로 연결 도구를 찾은 뒤 qualified tool을 호출합니다.
MCP 연결 예
import { defineMcpClientConnection } from "eve/connections";
import { once } from "eve/tools/approval";
export default defineMcpClientConnection({
url: "https://mcp.linear.app/mcp",
description: "Linear workspace: issues, projects, cycles, and comments.",
auth: {
getToken: async () => ({ token: process.env.LINEAR_API_TOKEN! }),
},
tools: { allow: ["search_issues", "get_issue"] },
approval: once(),
});좋은 connection definition은 네 가지를 명시합니다.
- description: 모델 routing용
- auth: token source와 principal scope
- allow/block filter: tool exposure 제한
- approval: write/sensitive action 게이트
Token은 모델에게 보이지 않는다
Connection token은 app runtime에서 resolve돼 outbound request에 주입됩니다. per-step cache는 되지만 durable state나 모델 history로는 직렬화되지 않습니다. 이게 Eve의 핵심 안전장치입니다.
다만 token scope를 잘못 주면 그래도 위험합니다.
| 위험 | 대응 |
|---|---|
| shared app token이 모든 tenant 데이터 접근 | user principal token 또는 tenant-scoped token |
| MCP 서버가 너무 많은 tool 노출 | allow-list |
| write operation이 approval 없이 실행 | connection approval: always() 또는 operation별 분리 |
| token revoked mid-call | provider 401을 ctx.requireAuth()로 매핑 |
Tool auth와 connection auth
개별 tool도 OAuth auth를 가질 수 있습니다. ctx.getToken()과 ctx.requireAuth()가 해당 tool의 auth 전략을 사용합니다.
| 사용 | 기준 |
|---|---|
| connection auth | 외부 MCP/OpenAPI 서버의 여러 tool을 묶어 인증 |
| tool auth | 특정 custom tool 하나가 OAuth를 필요로 함 |
| route auth | 사용자가 Eve route에 접근 가능한지 판단 |
셋은 서로 다른 layer입니다. route auth를 걸었다고 tool/connection token scope까지 안전해지지는 않습니다.
ask_question과 approval의 차이
ask_question은 모델이 사용자에게 되묻는 built-in tool이고, approval은 사람이 tool 실행을 허용하거나 거절하는 gate입니다. 둘 다 같은 input.requested pause protocol을 쓰지만 쓰임은 다릅니다.
| 목적 | 기능 |
|---|---|
| 실행 허가 | approval |
| 정보 부족 해결 | ask_question |
| OAuth consent | connection/tool auth |
Tool 설계 체크리스트
| 항목 | 기준 |
|---|---|
| input schema | 모든 필드 검증, enum/limit 명시 |
| output minimization | 모델에는 필요한 요약만 |
| idempotency | 외부 side effect에는 key/ledger |
| approval | irreversible/sensitive action은 always() 또는 predicate |
| auth | token scope와 principal type 명시 |
| observability | action.result hook으로 audit 가능 |
| eval | calledTool, noFailedActions, output schema 검증 |
도구는 에이전트의 손입니다. 손을 움직이는 프로토콜은 Eve가 주지만, 어떤 손에 얼마나 센 권한을 쥐여줄지는 작성자가 정합니다.