Ch7. Tools, Approval, Connections
Eve 도구 실행, 사람 승인, MCP/OpenAPI 연결, OAuth 흐름을 엔터프라이즈 보안 경계로 설계한다.
핵심 요약
- authored tool의
execute는 sandbox가 아니라 app runtime에서 돌아갑니다. privileged surface로 다뤄야 합니다. toModelOutput,needsApproval, auth scope는 tool이 어떤 데이터를 노출하고 어떤 위험 행동을 하는지 막는 핵심 장치입니다.- MCP/OpenAPI connection은 확장이 빠른 만큼 allowlist, approval, OAuth 책임 경계를 함께 설계해야 합니다.
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을 씁니다.
toModelOutput(output) {
return {
type: "text",
value: `Report ${output.id}: risk ${output.riskLevel}.`,
};
}엔터프라이즈에서는 이 패턴이 사실상 필수입니다. CRM row, audit payload, pricing table, PII를 모델에 그대로 돌려주면 후속 응답과 telemetry에 그 값이 섞여 들어갑니다.
승인 정책
Eve는 tool 단위로 needsApproval을 둡니다.
| 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(),
}),
needsApproval: always(),
async execute(input) {
return refundCharge(input);
},
});입력 기반 approval
금액, 대상, 계정 tier에 따라 승인 기준을 바꿀 수 있습니다.
needsApproval: ({ toolInput }) => {
const amount = Number(toolInput?.amount ?? 0);
return amount > 1000;
}실무에서는 predicate 안에 비즈니스 로직을 욱여넣지 말고 agent/lib/policy.ts에 순수 함수로 떼어 unit test를 붙입니다.
HITL은 durable park다
승인이 필요하면 Eve는 input.requested event를 내고 session을 session.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__<connection>__<tool> |
| OpenAPI | defineOpenAPIConnection | connection__<connection>__<operationId> |
모델은 먼저 connection__search로 연결 도구를 찾은 뒤 qualified tool을 호출합니다.
MCP 연결 예
import { defineMcpClientConnection } from "eve/connections";
import { once } from "eve/tools/approval";
export default defineMcpClientConnection({
url: "https://mcp.linear.app/sse",
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을 쓰지만 쓰임은 다릅니다.
| 목적 | 기능 |
|---|---|
| 실행 허가 | needsApproval |
| 정보 부족 해결 | 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가 주지만, 어떤 손에 얼마나 센 권한을 쥐여줄지는 작성자가 정합니다.