Ch9. Channels, Auth, Streaming
Eve channel이 세션 생성, continuation token, route auth, NDJSON stream을 어떻게 책임지는지 분석한다.
핵심 요약
- Channel은 인증, 입력 정규화, session/continuation token 소유권, response delivery를 책임지는 front door입니다.
- route auth는 fail-closed라도 session ownership ACL은 애플리케이션이 별도로 보장해야 합니다.
- continuation token은 queue가 아니므로 플랫폼마다 ordering과 retry 정책을 channel에서 설계해야 합니다.
Channel은 Eve 에이전트의 front door입니다. 모델 품질과 tool 안전장치를 아무리 잘 설계해도 channel이 인증·세션 소유권·전달 순서를 잘못 처리하면 프로덕션 사고가 납니다.
Channel의 세 가지 책임
| 책임 | 설명 |
|---|---|
| input normalization | Slack, HTTP, GitHub 등 플랫폼 입력을 user message로 바꿈 |
| continuation token ownership | 같은 대화/스레드를 어떤 token으로 재개할지 결정 |
| delivery | agent response, approval prompt, failure를 어디에 어떻게 보낼지 결정 |
Eve 기본 HTTP channel은 POST /eve/v1/session, POST /eve/v1/session/:sessionId, GET /eve/v1/session/:sessionId/stream을 제공합니다.
기본 Eve HTTP channel
agent/channels/eve.ts를 작성하지 않아도 framework default가 활성화됩니다. 보통 이 파일은 route auth를 바꿀 때 작성합니다.
import { eveChannel } from "eve/channels/eve";
import { localDev, vercelOidc } from "eve/channels/auth";
export default eveChannel({
auth: [localDev(), vercelOidc()],
});프로덕션 browser traffic을 받으려면 앱의 세션/JWT/OIDC/API key verifier를 추가해야 합니다. localDev()와 vercelOidc()만으로는 일반 사용자를 인증하지 못합니다.
공식 Auth & Route Protection 문서 기준으로는 scaffold와 framework default를 구분해야 합니다.
| 경우 | 기본 auth | 의미 |
|---|---|---|
agent/channels/eve.ts를 삭제한 framework default | [localDev(), vercelOidc()] | localhost와 Vercel OIDC caller만 허용하며 일반 production browser traffic은 거부됩니다. |
eve init scaffold가 만든 authored channel | [localDev(), vercelOidc(), placeholderAuth()] | production browser request에 구조화된 401을 반환해 "auth 미설정"을 드러냅니다. 실제 배포 전 교체해야 합니다. |
placeholderAuth()는 임시 guardrail이지 인증 정책이 아닙니다. 공개 데모가 아니라면 none()으로 열어두지 말고 앱의 session/JWT/OIDC/API key verifier를 넣으세요.
Route auth는 fail-closed다
Eve route auth는 ordered walk입니다.
| AuthFn 결과 | 의미 |
|---|---|
SessionAuthContext 반환 | 인증 성공, walk 종료 |
null/undefined 반환 | 다음 AuthFn으로 이동 |
| throw | 401/403 등으로 거부 |
| 모두 skip | 401 |
none()을 명시하지 않으면 익명 접근은 열리지 않습니다. scaffold의 placeholderAuth()도 production에서 401을 반환합니다.
Route auth와 session ownership은 다르다
Eve 문서가 명시하는 중요한 제한이 있습니다. route auth는 HTTP boundary에서 누가 요청할 수 있는지만 판단합니다. 여러 사용자/테넌트가 같은 route에 접근할 수 있다면 session ownership ACL은 직접 구현해야 합니다.
위험한 구조:
모든 로그인 사용자 -> 같은 Eve route 접근 가능
sessionId/continuationToken만 알면 follow-up 가능
tenant ownership 검증 없음권장 구조:
| 계층 | 기준 |
|---|---|
| route auth | 사용자/서비스 인증 |
| session mapping | app DB에 sessionId -> owner/tenant/channel 저장 |
| follow-up guard | current principal이 session owner/participant인지 확인 |
| stream guard | stream endpoint도 같은 ownership 검증 |
Custom channel을 만들면 route handler에서 이 검증을 직접 넣을 수 있습니다.
Continuation token은 queue가 아니다
continuationToken은 현재 session hook을 재개하는 handle입니다. 일반 FIFO queue가 아닙니다. 같은 session에 여러 메시지를 동시에 보내면 Eve가 특정 boundary에서 coalesce하기도 하지만, 채팅 큐처럼 deterministic ordering을 보장하지는 않습니다.
운영 패턴:
- 한 session에는
session.waiting이후 다음 turn을 보낸다. - burst가 오는 플랫폼은 channel/app 계층에 per-session queue를 둔다.
- 승인 응답과 새 메시지가 섞일 수 있음을 UI에 반영한다.
- 오래된 continuation token은 reject될 수 있으므로 클라이언트가 최신 token을 저장한다.
NDJSON stream
GET /eve/v1/session/:sessionId/stream은 newline-delimited JSON event를 제공합니다.
중요 event:
| Event | 운영 의미 |
|---|---|
session.started | durable session 생성 |
turn.started | 새 user turn 시작 |
step.started | model step 시작 |
actions.requested | tool/subagent call 요청 |
action.result | action 결과 |
input.requested | approval/question/OAuth 등 human input 필요 |
subagent.called | child session 연결 가능 |
message.appended | assistant text delta |
message.completed | assistant message 완성 |
result.completed | structured output 완성 |
step.failed/turn.failed/session.failed | 장애 경계 |
session.waiting | 다음 입력 대기 |
Reasoning event를 UI/로그에 표시할 때는 privacy와 retention 정책이 필요합니다.
Event dispatch order
한 event가 발생하면 다음 순서로 처리됩니다.
- channel handler가 실행되고 adapter state를 갱신한다.
- stream에 event가 durable하게 기록된다.
- hooks가 실행된다.
- dynamic tool/skill/instruction resolver가 실행된다.
이 순서를 활용하면 channel state 기반 dynamic capability를 구현할 수 있습니다. 예를 들어 Slack channel handler가 thread metadata를 갱신하면 dynamic instruction resolver가 그 team policy를 prompt에 넣습니다.
Custom channel 구조
import { defineChannel, POST } from "eve/channels";
export default defineChannel({
routes: [
POST("/incident", async (req, args) => {
const incident = await req.json();
args.waitUntil(
args.send(`Investigate incident ${incident.id}`, {
auth: {
authenticator: "incident-service",
principalType: "service",
principalId: incident.actorId,
attributes: { incidentId: incident.id },
},
continuationToken: `incident:${incident.id}`,
}),
);
return new Response("ok");
}),
],
});Custom channel에서 반드시 검토할 것:
- HMAC/JWT/OIDC 등 raw request authentication
- body-supplied principal 신뢰 금지
- constant-time signature compare
- request IP allow-list가 필요한지
- continuation token format 안정성
- file upload fetch policy
- failure event delivery 방식
공식 channel integration 표면
공식 문서는 HTTP channel뿐 아니라 여러 platform adapter를 따로 설명합니다. 엔터프라이즈 설계에서는 "어디로 응답하는가"보다 "어떤 identity와 private delivery를 보장하는가"를 봐야 합니다.
| Channel | 공식 문서 | 운영 포인트 |
|---|---|---|
| Eve HTTP | eve | session route, stream route, auth customization |
| Custom | Custom Channels | HTTP/WS route, metadata projection, cross-channel hand-off, file staging |
| Slack | Slack | thread anchoring, ephemeral auth challenge, Connect credential setup |
| Discord | Discord | slash command, components, modal, webhook verification |
| GitHub | GitHub | GitHub App webhook, @mention, PR diff context, sandbox checkout |
| Linear | Linear | Agent Sessions, Agent Activities, native progress/question/response UX |
| Teams | Microsoft Teams | Bot Framework Activity, Adaptive Card HITL prompt |
| Telegram | Telegram | bot webhook, inline-keyboard HITL, attachment handling |
| Twilio | Twilio | SMS/phone transcription, telecom compliance review |
Custom channel에서 공식 문서가 추가로 강조하는 고급 기능은 세 가지입니다.
| 기능 | 적용 기준 |
|---|---|
WS() routes | voice, realtime collaboration, vendor SDK adapter처럼 WebSocket이 필요한 channel |
args.receive(targetChannel, ...) | incident webhook이 Slack 조사 thread를 여는 등 cross-channel hand-off가 필요한 경우 |
metadata(state) | channel state 중 dynamic resolver/instrumentation에 노출해도 되는 값만 projection |
File upload 처리
send()는 text와 file part가 섞인 UserContent를 받습니다. Slack 등 외부 파일 URL은 channel fetchFile로 안전하게 fetch/stage합니다.
위험 기준:
| 위험 | 대응 |
|---|---|
| arbitrary URL fetch | platform domain allow-list |
| 대형 파일 | size/type policy |
| 민감 파일 retention | sandbox cleanup/retention |
| prompt injection document | downstream tool/action approval |
Client SDK 사용
공식 client 문서는 raw HTTP, stream loop, browser hook을 나눕니다. script/test/server-to-server는 TypeScript SDK를 쓰고, browser UI는 frontend overview의 useEveAgent 계열을 씁니다.
import { Client } from "eve/client";
const client = new Client({
host: "https://agent.example.com",
auth: {
bearer: async () => await getAccessToken(),
},
});
const session = client.session();
const response = await session.send("Run the production checklist.");
const result = await response.result();SDK를 쓰더라도 route auth와 session ownership은 서버 설계 문제입니다.
Browser hook 설계에서 중요한 공식 기준:
| 주제 | 기준 |
|---|---|
| resumable session | sessionId, continuationToken, streamIndex가 있는 session cursor 전체를 저장합니다. |
clientContext | 다음 model call에만 붙는 ephemeral context이며 durable session history에는 남지 않습니다. |
| HITL response | input.requested가 UI message의 dynamic-tool part metadata로 투영되고 같은 session에 inputResponses로 답합니다. |
| framework integration | Next.js withEve, Nuxt eve/nuxt, SvelteKit eveSvelteKit이 같은 origin에 Eve route를 붙입니다. |
채널 운영 체크리스트
| 항목 | 기준 |
|---|---|
| route auth | production에서 placeholder 제거, fail-closed 확인 |
| session ACL | sessionId와 principal/tenant 매핑 |
| stream ACL | stream endpoint도 보호 |
| continuation | stale token 처리, 최신 token 저장 |
| ordering | per-session queue 필요 여부 |
| metadata | dynamic resolver에 노출해도 되는 값만 project |
| signatures | constant-time verify |
| eval | unauthenticated 401, wrong tenant 403, valid stream success |
채널은 단순한 transport adapter가 아니라 agent의 보안 perimeter입니다.