콘텐츠로 이동

evo-dispatch 웹 연동 설정

원천 · framework/evomind/web/workflow-project-evo-service기준 · 릴리스 v0.12.0커밋 · 2e32c6cc파일 · docs/operations/dispatch.md기준선 · 2026-11-12

웹 대화 하나를 evo-dispatch 세션 하나로 보이게 해서, 같은 사용자의 Evo CLI·다른 웹 대화와 작업 요청을 주고받는다(결정, 구현 ~). 채팅 모델도 사용자 승인을 받아 요청·답장을 보낼 수 있다(8절). 기본은 꺼짐이다. 꺼진 상태에서는 화면에 dispatch 표시가 없고 기존 동작과 같다.

브라우저 ─(앱 자체 토큰)─▶ evo-api /api/dispatch/* ─(사용자 본인 Keycloak access token, azp evo-web)─▶ evo-dispatch
└─ 20초마다 POST /v1/presence, 요청·목록은 POST /mcp(stateless tools/call)
  • 브라우저는 dispatch 주소와 Keycloak 토큰을 모른다. evo-api 가 SSO 로그인 때 받은 사용자 본인의 토큰으로 대신 호출한다.
  • 세션 = 사용자 + evo-web + 대화 UUID. 작업 공간 이름은 web 으로 고정이고, 작업 담당 이름(agent)은 대화마다 화면에서 정한다.
  • 받은 요청은 실행 명령이 아니다. 사용자가 고른 요청을 입력창에 첨부하고 지시를 써서 보낼 때만 모델에 전달된다.
항목 조건
로그인 방식 SSO(OIDC) 로그인 계정만 쓸 수 있다(web-oidc.md). 로컬·LDAP 계정에는 “SSO 계정 전용” 안내만 보인다.
dispatch 서버 발급자 dispatch 서버의 OIDC_ISSUER_URI 에 이 인스턴스 Keycloak 의 발급자(https://<keycloak>/realms/evo)가 있어야 한다.
dispatch 허용 클라이언트 dispatch 서버의 OIDC_ALLOWED_AZP 에 웹 클라이언트 ID(evo-web)가 있어야 한다. 예: evo-cli,evo-web. 없으면 화면 상태가 “dispatch 가 거부함”이 된다.
사용자 구분 dispatch 는 토큰의 iss+sub 로 사용자를 구분한다. 같은 렐름의 evo-web·evo-cli 토큰은 iss+sub 가 같아 같은 사용자의 세션으로 보인다.
Keycloak 클라이언트 웹 로그인용 evo-web 을 그대로 쓴다. 추가 클라이언트·offline_access 는 필요 없다.

.env(개발서버는 인스턴스별 CI/CD File 변수, instances.md)에 넣고 다시 배포한다.

변수 기본 설명
DISPATCH_ENABLED false true 이고 DISPATCH_URL 이 있을 때만 켜진다.
DISPATCH_URL 비움 dispatch 서버 기본 주소. 예: https://<dispatch 주소> (/v1·/mcp 는 붙이지 않는다)
DISPATCH_HEARTBEAT_INTERVAL PT20S 활성 신호 주기. dispatch 규격 허용 범위는 15~30초다.
DISPATCH_LEASE_TTL PT75S 브라우저 탭이 대화를 붙잡고 있는 시간. 화면이 20초마다 갱신하고, 이 시간 동안 갱신이 없으면 놓은 것으로 본다.
DISPATCH_TIMEOUT PT5S dispatch·토큰 갱신 호출 시간 제한
DISPATCH_MAX_ACTIVE_CONVERSATIONS 10 사용자당 동시에 활성(임대 중)일 수 있는 대화 수. 탭을 합쳐 센다. 넘는 새 대화 임대는 409 too_many_active 이고, 이미 활성인 대화의 갱신은 막지 않는다. 0 이하면 제한 없음. 기본값만 쓰면 따로 넣지 않아도 된다(바꾸려면 서버 compose 사본이 이 변수를 넘겨야 하므로 evo-infra 재설치가 필요하다).

DB 마이그레이션 V21(대화별 작업 담당 이름 열)이 앱 기동 때 자동으로 적용된다.

상황 동작
대화를 연다 누르면 바로 열리고, 그 탭이 대화를 임대한다. evo-api 가 바로 heartbeat 를 보내고 이후 20초마다 보낸다.
여러 대화 웹은 열기·닫기가 없다. 한 탭에서 본 대화는 다른 대화로 옮겨도 임대를 놓지 않아 계속 활성이고, 한 탭 안의 대화끼리 요청을 주고받는다. 생성 중인 답도 뒤에서 이어 간다. 탭마다 최근 본 대화 10개까지 유지하고, 넘으면 가장 오래 안 본 대화부터 놓는다(생성 중인 대화는 되도록 남긴다). 목록은 그 탭에만 기억한다(새로고침하면 이어지고, 탭을 닫으면 사라진다). 사이드바 행에는 생성 중(초록 점)·도구 승인 대기(주황 점)·안 읽은 수를 보인다. 서버 상한(다른 탭과 합쳐 10개)에 걸리면 그 대화는 연결하지 않고 안내만 보이며 채팅은 그대로 된다.
대화를 지운다, 탭을 닫는다, 로그아웃한다, 10개를 넘어 밀려난다 그 대화의 임대를 놓고 생성 중이던 답을 멈춘다. 같은 대화를 연 다른 탭이 없으면 end 를 보낸다.
탭이 갑자기 꺼진다 75초 뒤 임대가 풀려 end 를 보낸다(그 전이라도 dispatch 는 마지막 신호 90초 뒤 종료로 본다).
로그아웃한다 그 사용자의 열린 세션에 모두 end 를 보내고 보관 토큰을 지운다.
계정을 비활성화한다 다음 주기(20초 안)에 그 사용자의 세션에 end 를 보내고 토큰을 지운다.
access token 만료가 가깝다 만료 30초 전에 refresh token 으로 새로 받는다. 신원(iss+sub)이 바뀌었거나 갱신이 실패하면 토큰을 지우고 상태가 “SSO 다시 로그인 필요”가 된다.
evo-api 를 재기동한다 토큰은 메모리에만 있어 사라진다. 사용자가 SSO 로 다시 로그인하기 전까지 상태가 “SSO 다시 로그인 필요”이고, 머리글 패널의 링크로 다시 로그인할 수 있다.
dispatch 가 꺼져 있거나 응답이 없다 상태가 “연결 안 됨”이 되고 채팅은 그대로 된다. 다음 주기에 다시 시도한다.

토큰 수명은 Keycloak 설정을 따른다. 임대가 있는 동안에는 주기적으로 갱신하므로 SSO 세션 유휴 시간(SSO Session Idle)이 지나도 끊기지 않지만, SSO 세션 최대 시간(SSO Session Max)이 지나면 다시 로그인해야 한다.

  • Keycloak 토큰은 evo-api 메모리에만 둔다. DB·파일·로그·응답·브라우저에 남기지 않는다. 12시간 동안 쓰지 않은 토큰은 지운다.
  • 토큰의 iss+sub 가 그 사용자의 OIDC 신원과 같을 때만 보관·사용한다.
  • 모든 /api/dispatch/* 호출은 로그인 사용자의 자기 대화에 대해서만 한다. 다른 사용자의 대화는 찾을 수 없음(404), 로컬·LDAP 계정은 403 이다.
  • 요청 본문과 토큰은 evo-api 로그에 남기지 않는다(dispatch 규격 §9). 받은 요청 본문은 화면에서 HTML·이미지 없이 보이고, 링크는 http(s) 만 새 창으로 연다.
  • 요청에 비밀정보(토큰·비밀번호·내부 IP)를 넣지 않도록 보내기·답장 창에 안내한다.
  1. dispatch 담당에게 OIDC_ALLOWED_AZP 에 evo-web 이 들어갔는지 확인한다(D1).
  2. DISPATCH_ENABLED=true, DISPATCH_URL 을 넣고 배포한다.
  3. SSO 계정으로 로그인해 대화를 연다. 대화 머리글에 이름 없음 · web · 연결됨 이 보이면 된다.
    • “dispatch 가 거부함”: dispatch 의 허용 클라이언트·발급자 설정을 확인한다.
    • “SSO 다시 로그인 필요”: evo-api 재기동 뒤라면 정상이다. SSO 로 다시 로그인한다.
    • “연결 안 됨”: DISPATCH_URL 과 dispatch 서버 상태(GET <dispatch>/actuator/health)를 확인한다.
  4. 같은 계정으로 Evo CLI 를 켜고 /dispatch list 를 실행하면 웹 대화가 evo-web 세션으로 보인다. 머리글 패널의 “요청 보내기”에서 CLI 세션을 골라 보내고, CLI 에서 받은 요청을 확인한다.
  5. evo-api 로그에 토큰·요청 본문이 없는지 확인한다.
경로 설명
GET /api/dispatch/status 계정 상태: disabled · unsupported · sso_required · ready
POST /api/dispatch/presence 탭 임대·갱신(conversationId, tabId, deviceId). 응답은 대화 세션 상태와 안 읽은 수. 사용자당 상한을 넘는 새 대화면 409 too_many_active
GET /api/dispatch/presence?conversationId= 지금 상태(신호를 보내지 않음)
POST /api/dispatch/presence/release 탭 임대 놓기
PUT /api/dispatch/agent 대화별 작업 담당 이름(빈 값이면 지움). 바꿔도 세션은 그대로
GET /api/dispatch/inbox?conversationId=&limit= 받은 요청 목록(읽음 표시 없음)
POST /api/dispatch/read 고른 요청 하나 읽기(받는 세션이면 읽음이 된다)
GET /api/dispatch/sessions?conversationId= 내 활성 세션 목록
POST /api/dispatch/send 보내기·답장(주소 user·agent·workspace·session, 제목 200자, 본문 16KB, 링크 20개)

오류는 {"status","error","message"} 로 답한다. dispatch 규격 오류 코드(no_active_recipient 등)를 그대로 싣고, 화면이 로그아웃으로 오해하지 않도록 dispatch 인증 거부도 401 이 아니라 502 dispatch_rejected_token 으로 답한다.

웹 사용자가 dispatch 버그 신고 공간(/mcp/bugs, evo-dispatch docs/protocol/bugs.md v2)에 버그를 올리고(화면 캡처 최대 3장), 자기가 올린 버그의 처리 상태와 처리 결과를 본다. 계정 메뉴의 버그 신고에서 연다. 기본은 꺼짐이다.

의 공용 요청 공간(/mcp/board, 프로젝트 앞으로 요청 올리기·취소) 화면을 대신한다. dispatch 가 그 공간을 개발 에이전트 전용 개발 요청 공간(/mcp/dev, 가져가기 키만)으로 바꾸고 옛 주소를 없애므로(evo-dispatch#41·#42), 웹은 제품 사용자용 버그 신고 공간을 쓴다.

브라우저 ─▶ evo-api /api/dispatch/bugs* ─(사용자 본인 Keycloak access token)─▶ evo-dispatch POST /mcp/bugs (stateless tools/call)
  • 대화 세션과 상관없다. 활성 신호·세션 header 를 보내지 않고, 위 1~4절의 토큰 보관을 그대로 쓴다(SSO 계정만).
  • dispatch 는 토큰의 iss+sub 로 사용자를 가린다. 조직용·일반용 로그인은 서로 다른 사용자이고, 자기가 올린 버그만 본다.
  • 올린 쪽에는 본문·이미지와 처리한 쪽 정보가 돌아오지 않는다. evo-api 도 본문·이미지 사본을 두지 않고 로그에 남기지 않는다.
  • 버그는 분류(product)를 골라 올린다(bugs.md v2 §2). 기본은 웹 채팅 CHAT 이고, 사용자가 다른 제품 버그라고 보면 CLI(Evo CLI)·EMULATOR(에뮬레이터)·MAC·WINDOWS(데스크톱 앱)·DISPATCH(dispatch)를 고른다. CHAT 은 evo-service 가 처리한다.
  • 처리는 그 분류를 맡은 프로젝트의 개발 에이전트가 개발 요청 공간 키로 한다(bug_take·bug_close). 처리 쪽에는 올린 사람 신원이 보이지 않고 로그인 클라이언트(evo-web)만 보인다.
  • version 은 evo-api 빌드 버전을 서버가 채우고, os 는 브라우저가 알려 준 운영체제(예: Windows (web))다.
  • 첨부 이미지: PNG·JPEG·WebP, 최대 3장, 장당 1MiB. 화면이 1MiB 를 넘는 이미지를 긴 변 1600px JPEG 로 줄여 보낸다. evo-api 는 base64·크기·파일 앞부분 서명이 형식과 맞는지 dispatch 를 부르기 전에 검사한다. 보내기 전에 미리보기를 보여 주고 비밀정보·개인정보가 찍히지 않게 안내한다.
변수 기본 설명
DISPATCH_BUGS_ENABLED false true 이고 DISPATCH_ENABLED·DISPATCH_URL 이 있을 때만 켜진다. dispatch 서버의 BOARD_ENABLED 도 켜져 있어야 한다. 서버 쪽이 꺼져 있으면 화면에 “dispatch 서버에서 버그 신고가 꺼져 있습니다”가 보인다. compose 는 값이 없으면 옛 DISPATCH_BOARD_ENABLED 를 이어받는다.
DISPATCH_BUGS_PRODUCTS CHAT,CLI,EMULATOR,MAC,WINDOWS,DISPATCH 고를 수 있는 버그 분류(쉼표 구분, 첫 값이 기본, 형식 ^[A-Z][A-Z0-9_]{0,31}$). dispatch 서버 BUGS_PRODUCTS 의 분류 안에 있어야 저장된다(없으면 dispatch 가 400 invalid_request 와 사유). 분류가 둘 이상이면 화면에 분류 선택이 보인다.
DISPATCH_BUGS_TIMEOUT PT30S bug_report·bug_mine 호출 시간 제한. 이미지를 실어 보내므로 DISPATCH_TIMEOUT 보다 길다.

옛 DISPATCH_BOARD_PROJECTS 는 쓰지 않는다.

경로 설명
GET /api/dispatch/bugs/status enabled, status(disabled·unsupported·sso_required·ready), 고를 수 있는 분류 products
GET /api/dispatch/bugs?limit= 내가 올린 버그, 최근 순(1~50, 기본 20): id·product·title·status(open 접수·taken 확인 중·closed 처리 끝)·note(처리 끝일 때 결과)·imageCount·createdAt·updatedAt
POST /api/dispatch/bugs 신고: product(분류, 목록 안), title(200자), body(16384자), 선택 os(100자), 선택 images(최대 3, {mimeType, data(base64)})

입력은 dispatch 를 부르기 전에 검사한다(400). dispatch 가 입력 형식 오류를 글자로 돌려주면 그 사유를 400 으로 전한다. dispatch 서버의 버그 신고 공간이 꺼져 있으면 503 bugs_disabled, 한도(사용자마다 1시간 10건)를 넘으면 429 rate_limited 다. 올린 사람이 취소하는 기능은 없다(잘못 올렸으면 처리 쪽이 메모와 함께 닫는다).

dispatch 가 켜진 SSO 대화에서는 채팅 모델이 Evo CLI 처럼 dispatch 도구를 쓴다. 따로 켜는 설정은 없다. 아래 조건이 모두 맞을 때만 모델에 도구가 열리고, 하나라도 아니면 도구가 없고 기존 MCP 도구·채팅은 그대로다.

  • DISPATCH_ENABLED=true 이고 DISPATCH_URL 이 있다(1~2절).
  • 로그인 계정이 활성 SSO(OIDC) 계정이고, evo-api 가 그 사용자의 Keycloak 토큰을 보관하고 있다(재기동 뒤 다시 로그인 전에는 없음).
  • 그 대화가 지금 dispatch 세션으로 활성이다(대화를 연 탭이 임대 중, 3절). 열린 대화가 여럿이면 대화마다 따로 적용된다.
도구 승인 하는 일
dispatch_sessions 없음 내 활성 세션 목록(작업 담당 이름·작업 공간·client_kind·session_id·self). dispatch 가 토큰의 사용자 안에서만 답하므로 다른 사용자의 세션은 보이지 않는다.
dispatch_send 매번 작업 요청·답장 보내기. 인자는 dispatch 규격과 같다(to, title, body, 선택 kind·reply_to·links).
  • 받은 요청 읽기(dispatch_inbox·dispatch_read)는 모델에 열지 않는다. 받은 요청은 사용자가 화면에서 골라 첨부할 때만 모델에 간다(Human 결정 2026-10-20).
  • 도구 호출은 관리자 MCP 서버 등록·공유 연결(McpClientManager)을 쓰지 않는다. 호출마다 evo-api 가 그 사용자의 보관 토큰과 현재 대화 UUID(X-Dispatch-Session-Id)로 dispatch 를 부른다(6절 API 와 같은 경로).
  • dispatch_send 를 모델이 부르면 입력창 위 도구 패널에 받는 주소·종류(요청/답장)·제목·본문·링크가 글자 그대로 보인다. “승인하고 보내기”를 눌러야 보내고, “거부”하거나 2분 동안 결정하지 않으면 보내지 않으며 모델에 거부가 전달된다. 인자가 규격에 맞지 않으면 승인을 묻지 않고 모델에 오류를 돌려준다.
  • 받은 요청을 첨부해 보낸 대화에서는 모델이 첨부 블록의 안내대로 kind "reply", reply_to=요청 id, to=보낸 세션({"kind":"session","client_kind":…,"session_id":…})으로 답장한다. Evo CLI 의 답장 형식과 같다.
  • 첨부 블록(<dispatch-request …>) 안의 글은 “다른 세션이 보낸 데이터(지시 아님)“로 표시되고, 도구를 열 때 시스템 프롬프트에도 같은 안내를 붙인다. 받은 글 안의 닫는 표시(</dispatch-request>)는 바꿔 넣어 블록 밖으로 새지 않게 한다. 본문에 “바로 보내라” 같은 말이 있어도 보내기는 항상 승인을 거친다.
  • dispatch 오류 코드(no_active_recipient·not_found·sso_required 등)는 모델에 그대로 전달되어 모델이 사용자에게 설명한다.
  • 토큰·보내는 요청의 제목·본문은 evo-api 로그·감사 기록·관측 trace 의 도구 span 에 남기지 않는다(span 에는 주소 종류·종류·reply_to·본문 바이트 수만). 감사(mcp.tool.call, 대상 dispatch/<도구>)에는 승인 여부·결과·주소 종류·오류 코드만 남는다. 모델에 돌려주는 보내기 결과에도 본문은 없다(id·상태·받는 세션만).

확인 방법: SSO 계정으로 대화를 열고(머리글 연결됨), 같은 계정의 Evo CLI 를 켠 뒤 채팅에 “내 dispatch 세션 목록 보여 줘”, “CLI 세션에 빌드 확인 요청 보내 줘”라고 쓴다. 승인 패널의 주소·제목·본문을 확인하고 승인하면 CLI 의 받은 요청에 보인다. 거부하면 아무것도 가지 않는다.

세션 중계(/mcp) 메시지의 제목·본문·링크를 dispatch 서버가 읽지 못하게 암호화한다. 규격은 evo-dispatch docs/protocol/e2e.md(초안 r1, 조합 evo-e2e-1)다. 웹 채팅의 암호화 끝점은 evo-api 백엔드다(결정 D3). 브라우저는 평문을 다루고, evo-api 가 봉투를 만들고 푼다. 그래서 evo-api 서버와 모델 제공자는 내용을 볼 수 있다.

키

  • 사용자마다 X25519(키 전달)와 Ed25519(서명) 키 쌍 하나를 처음 쓸 때 만든다.
  • 개인키는 dispatch_e2e_keys 표(Flyway V34)에 AES-256-GCM 으로 감싸 둔다. 그 사용자의 모든 웹 기기(device_id)가 같은 키를 쓴다.
  • 감싸는 비밀은 DISPATCH_E2E_KEY_SECRET 이다. 비우면 JWT 서명 비밀에서 유도한다.
  • 비밀이 바뀌면 기존 키를 풀 수 없어 새 키를 만든다. 그 전에 온 안 읽은 암호 메시지는 읽을 수 없다(규격이 받아들이는 손실).

활성 신호: 공개키(e2e.kem_public·sign_public)를 싣는다.

보내기

  1. 보내기 직전에 dispatch_sessions 로 받는 세션(규격 §3)과 공개키를 구한다.
  2. 받는 세션이 모두 키가 있으면 봉투(enc)만 보낸다. 받는 기기에는 자기 기기도 넣는다.
  3. 세션 요청은 늘 암호화한다(evo 프로그램 공통 결정). 평문으로 보내는 경로는 없다. 다음 경우에는 보내지 않는다.
    • 키가 없는 받는 세션이 하나라도 있다: 409 e2e_unavailable(키가 없는 세션 이름 포함).
    • 받는 세션이 없다: 409 no_active_recipient.
    • 이 서버에서 암호화가 꺼져 있다(DISPATCH_E2E_ENABLED=false): 409 e2e_unavailable. 받기는 그대로 된다.
    • 화면과 모델 도구는 이유만 보인다. 개발 요청·버그 신고 공간은 평문 그대로다(범위 밖).
  4. recipient_mismatch 가 오면(그사이 받는 세션이 바뀜) 세션을 다시 구해 3번까지 보낸다.

받기·읽기

  • enc 메시지는 서명을 먼저 확인한 뒤 내용 키를 풀고, 제목·본문·링크로 바꿔 화면·모델에 넘긴다(e2e.status: verified). 봉투와 보낸 이 공개키는 넘기지 않는다.
  • 서명·복호가 실패하면 내용 없이 e2e.status: failed(reason)로 넘긴다. 화면은 “열 수 없는 암호화 요청”으로 보이고 채팅에 붙일 수 없다.
  • 보낸 시각이 서버 기록과 10분 넘게 다르면 time_skew_minutes 를 붙여 화면에 알린다.
  • 평문 메시지는 그대로 받는다(전환 기간 호환).

아직 하지 않은 것(규격 후속): 기기 지문 확인과 처음 본 키 기억(TOFU) 경고, 재전송 중복 저장 탐지, 키 백업·폐기 화면.

변수 기본 설명
DISPATCH_E2E_ENABLED true 끄면 공개키를 싣지 않고 세션 요청을 보낼 수 없다(평문 받기만 됨). 암호 메시지는 열지 않는다(화면에는 “열 수 없는 암호화 요청”).
DISPATCH_E2E_KEY_SECRET (빈 값) 기기 개인키를 감쌀 비밀이다. 비우면 JWT 서명 비밀에서 유도한다. 로그·응답에 쓰지 않는다.