어댑터 온보딩 · 런타임 준비 상태
작성 시각: 2026-09-13 · 상대 저장소 실사 기반(PortalConsole, ProcessCenter) 관련 계약: docs/control-plane-contract.md · docs/SERVICE_BOUNDARIES.md
1. 현황 요약
구현됨(라이브러리)
| 구성 | 위치 | 내용 |
|---|---|---|
| 계약 타입 | src/control-plane.ts | CONTRACT_VERSION="1", EXPLORE_CAPABILITIES=["search","answer"], catalog 응답 검증 |
| Control plane 클라이언트 | src/control-plane.ts | GET {origin}/api/directory/v1/control-plane/catalog(Bearer API key, 기본 1.5s), 계약 위반 시 오류 |
| Adapter registry | src/registry.ts | register()(portalId 형식 검증) · search()(catalog ∩ allowed ∩ 등록 어댑터, 병렬 실행·Promise.allSettled·포털별 timeout) · ask()(answer 능력 어댑터를 순서대로 시도해 첫 답 반환) · searchUsingCatalog()(TTL 30s·stale 300s·catalogStale) |
| HTTP 어댑터 | src/http-adapter.ts | 원격 어댑터 호출(Authorization: Bearer <서비스 토큰>, x-explore-subject-id, x-explore-request-id, {query,limit} → {results}), 포털 ID 자기검증 필터 · answer()(answerEndpoint 로 {question} → {answer,citations}, 빈 답은 null) |
| 반환 형태 | src/types.ts | results·partial·unavailablePortals·requestId (소비 포털의 기존 계약과 동일) |
| 테스트 | src/*.test.ts | control plane·registry·http adapter |
구현됨(런타임, 2026-09-13) ✔
| 구성 | 위치 | 내용 |
|---|---|---|
| HTTP API | src/server.ts | GET /healthz(구성된 어댑터 노출), POST /api/explore/v1/search(zod 검증 → 사용자 토큰 인증 → results·partial·unavailablePortals·catalogStale·requestId 반환), POST /api/explore/v1/ask({question} → answer·citations·providerId), 401/400/404/500 처리 |
| 사용자 인증 | src/auth.ts | PortalConsole 사용자 토큰 검증(HS256, iss=directory-service, aud=directory-user) → sub를 subjectId로 어댑터에 전달 |
| Composition root | src/runtime.ts | catalog 조회 → 포털별 어댑터 자동 구성(PORTAL_SERVICE_TOKENS_JSON에 토큰이 있는 포털만), 캐시·stale 전파, search()·ask() |
| 배포 자산 | Dockerfile.dev · .env.example · 스크립트 | pnpm dev/start/build, PORTAL_CONSOLE_JWT_SECRET·CONTROL_PLANE_*·PORTAL_SERVICE_TOKENS_JSON |
| 테스트 | src/server.test.ts · src/runtime.test.ts | 인증 401 · 검증 400 · subject 전달 · catalog 기반 어댑터 구성 · 부분 실패 · stale (전체 5개 파일 통과) |
남은 운영 작업: 카탈로그 등록(§2, PortalConsole 설정) · 포털 토큰 교환(§4) · 탐색 UI 노출(§6-3).
2. 카탈로그 등록 (PortalConsole — 이미 구현됨)
PortalConsole는 server/controlPlaneCatalog.ts에서 GET /api/directory/v1/control-plane/catalog를 이미 제공한다.
- 소스:
portal_configurations(활성 포털) +PORTAL_EXPLORE_CATALOG_JSON(포털별 탐색 정책) - 기본값(정책 미지정):
capabilities=["search"],endpoint="/api/explore/v1/search",timeoutMs=1500— ProcessCenter 어댑터 경로와 일치 portals[]에 포함되는 것:applicationType이CONSUMER_PORTAL인 포털 + 정책에서answer능력을 선언한 서비스(예: RagServicerag-center). 그 외는applications[]에만 들어간다 — 게이트웨이의 answer provider 는 포털이 아닐 수 있다(§3-1).answer능력을 선언한 항목은explore.answerEndpoint(기본/api/explore/v1/ask)를 함께 광고한다.
ProcessCenter 등록 예시(운영 설정)
// PORTAL_EXPLORE_CATALOG_JSON (PortalConsole 환경변수)
{
"process-center": { "capabilities": ["search"], "endpoint": "/api/explore/v1/search", "timeoutMs": 1500 }
}전제: portal_configurations에 process-center가 있고 enabled=true, applicationType=CONSUMER_PORTAL, baseUrl이 게이트웨이에서 접근 가능해야 한다.
RagService answer provider 등록 예시(운영 설정)
// PORTAL_EXPLORE_CATALOG_JSON (PortalConsole 환경변수)
{
"process-center": { "capabilities": ["search"], "endpoint": "/api/explore/v1/search", "timeoutMs": 1500 },
"rag-center": { "capabilities": ["search", "answer"], "endpoint": "/api/explore/v1/search", "answerEndpoint": "/api/explore/v1/ask", "timeoutMs": 8000 }
}rag-center의search(문서 검색)는 결과를kind: "document"로 돌려줘 상단 검색 결과에 합류한다(POST /api/explore/v1/search).answer는 근거 답변(POST /api/explore/v1/ask). 둘 다 이 서비스가 사용자 토큰으로 RLS 를 직접 강제한다.
전제: portal_configurations 에 rag-center(enabled=true, baseUrl이 게이트웨이에서 접근 가능; applicationType은 PLATFORM_SERVICE여도 무방)가 있어야 한다. 게이트웨이 PORTAL_SERVICE_TOKENS_JSON 에 그 포털 토큰이 있어야 원격 어댑터가 등록되고, RagService 는 같은 토큰을 RAG_EXPLORE_GATEWAY_API_KEY 로 받는다(§3-1 의 Authorization 헤더).
3. 어댑터 계약 (포털이 제공)
포털은 다음을 제공한다(ProcessCenter 구현 완료: ProcessCenter/server/remoteExplore.ts, 계약 ProcessCenter/docs/EXPLORE_ADAPTER_CONTRACT.md).
| 항목 | 규칙 |
|---|---|
| 메서드·경로 | POST <explore.endpoint> (기본 /api/explore/v1/search) |
| 인증 | Authorization: Bearer <포털이 게이트웨이에 발급한 서비스 토큰> — 불일치 시 401 |
| 사용자 | x-explore-subject-id 필수 — 포털이 자기 정책으로 최종 권한 강제 |
| 상관관계 | x-explore-request-id(선택) |
| 요청 | { "query": string(1~200), "limit": number(1~30) } |
| 응답 | { "results": [{ id, kind, portalId, portalLabel, title, summary?, meta?, href? }] } |
| 실패 | 5xx·timeout 시 게이트웨이가 해당 포털을 unavailablePortals로 기록하고 partial=true |
3-1. 답변(answer) 제공자 계약 (RagService 소유)
answer capability가 켜진 포털은 POST <explore.answerEndpoint>(예: /api/explore/v1/ask)로 질문을 받는다. 인증·사용자 헤더는 검색과 동일하다.
| 항목 | 규칙 |
|---|---|
| 인증 | Authorization: Bearer <서비스 토큰> · x-explore-subject-id 필수 · x-explore-user-token(게이트웨이가 검증한 사용자 토큰; provider 가 RLS 를 직접 강제할 때 사용) |
| 요청 | { "question": string(2~2000) } |
| 응답 | { "answer": string, "citations"?: [{ "title": string, "href"?: string }] } — 답을 못 만들면 빈 answer |
| 권한 | 원본 서비스(RagService)가 조직·보안등급으로 최종 필터 (게이트웨이는 대체하지 않음) |
| 등록 | catalog 에 capabilities: ["answer"] + explore.answerEndpoint (PortalConsole PORTAL_EXPLORE_CATALOG_JSON) |
게이트웨이는 후보 provider 를 순서대로 시도해 첫 답에서 멈추므로, 여러 RAG 제공자를 둘 때는 catalog 등록 순서가 우선순위가 된다.
3-2. RAG 정본과 폴백 (역할 정리)
- 정본은 중앙 RAG(RagService
rag-center) 다. 상단 검색의 질문은 게이트웨이/api/explore/v1/ask→ RagService 로 가고, 문서 검색은search어댑터(kind=document)로 결과에 합류한다. 게이트웨이는 답을 만들지 않는다(오케스트레이션만). - 소비 포털은 게이트웨이 우선, 자기 색인 폴백을 쓴다(구현 선례):
포털 답변 절차 폴백 ProcessCenter explore.gatewayAsk(server/exploreGatewaySearch.ts)로컬 RAG( answerPrivateRag) —DISABLE_LOCAL_RAG_FALLBACK=true로 끌 수 있다AgentCenter explore.ask(server/application/exploreAskService.ts)자기 색인 + ModelGateway( exploreAnswer)- 검색은 위임하지 않는다: 셸 검색은 각 포털 색인(
explore.search)이 즉시 돌려주고, 답변만 게이트웨이가 만든다. - 답을 만든 주체는 응답의
providerId로 남는다(어느 쪽이 답했는지 감추지 않는다).
- 검색은 위임하지 않는다: 셸 검색은 각 포털 색인(
- 위임 토큰은 경로 B(서버 위임 발급) 를 쓴다: 포털이 자기
client_id·client_secret으로 그 사용자의 좁힌 토큰을 받아 게이트웨이에 전달한다. PortalConsole 의PORTAL_CONSOLE_TOKEN_CONSUMERS_JSON에 소비자별delegationAudiences(예:["explore-gateway"])·delegationScopes(예:["search:read"])를 등록해야 한다. - 모델 접근은 ModelGateway 소유다. RagService 는
MODEL_GATEWAY_URL·MODEL_GATEWAY_SERVICE_TOKEN이 설정되면 로컬 vLLM 대신 게이트웨이를 경유하고 목적(x-purpose: rag.answer)만 보낸다 — 미설정 시에만 로컬 vLLM 으로 폴백한다.
4. 키 주입
| 키 | 소유·사용 | 주입 방식(제안) |
|---|---|---|
| DS portal API key | 게이트웨이 → PortalConsole catalog 조회(directory:read) | 게이트웨이 env CONTROL_PLANE_API_KEY |
| 포털별 서비스 토큰 | 게이트웨이 → 각 포털 어댑터 호출 | 초기: 게이트웨이 env PORTAL_SERVICE_TOKENS_JSON = { "process-center": "<token>" } · 후속: DS 관리 키 원장/로테이션 |
| JWT 서명키 | 게이트웨이 → 사용자/위임 토큰 검증 | 게이트웨이 env PORTAL_CONSOLE_JWT_SECRET = DirectoryService PORTAL_JWT_SECRET 와 동일 값 |
| 토큰 audience | 게이트웨이 → 위임 토큰 검증 | 게이트웨이 env PORTAL_CONSOLE_JWT_AUDIENCE = 소비 포털이 요청하는 위임 audience. PC 는 EXPLORE_GATEWAY_AUDIENCE(기본 explore-gateway)로 발급하므로 게이트웨이 기본값 directory-user 로 두면 401 이다 — 반드시 explore-gateway 로 맞춘다. |
포털 쪽 대응 키: ProcessCenter EXPLORE_GATEWAY_API_KEY(동일 값). 로테이션은 양쪽 동시 변경이 필요하므로 다중 토큰 허용(서비스 토큰 일반화, PortalConsole/docs/delegation-token-contract.md §5-1)과 함께 설계한다.
5. 검증 체크리스트
- [x] 어댑터 단독 검증 — 무토큰 401 · subject 누락 400 · 정상 200(결과 형태 일치) — ProcessCenter:
server/remoteExplore.test.ts통과 + 개발 스택 실호출 확인 - [x] 게이트웨이 HTTP API 구현 —
src/server.ts(요청 검증·subject 인증·응답 정규화) - [x] Composition root —
src/runtime.ts(catalog 조회 후 포털별 어댑터 자동 등록, 토큰 없는 포털은unavailablePortals) - [x] Catalog 응답 확인 —
process-center가portals[]에capabilities=["search"]로 포함(개발 스택 확인) - [x] 부분 실패 — 어댑터 중단 시
partial=true·다른 결과 유지(런타임 테스트) - [x] stale — catalog 조회 실패 시 캐시 사용 +
catalogStale=true(런타임 테스트) - [x] e2e — 게이트웨이 API 호출 → ProcessCenter 결과 표시(개발 스택, 아래 §5-1)
5-1. 개발 스택 검증 결과 (2026-09-13)
ProcessCenter/docker-compose.dev.yml에 explore-gateway(호스트 3500)를 등록하고 실제 호출까지 확인했다.
| 항목 | 값·결과 |
|---|---|
| 게이트웨이 헬스 | {"status":"ok","version":"0.1.0","adapters":["process-center"],"configuredPortals":["process-center"]} — 어댑터는 첫 탐색 시 catalog 기준으로 등록된다(그 전에는 adapters: []) |
| e2e 호출 | POST http://localhost:3500/api/explore/v1/search + HS256 사용자 토큰(aud=directory-user) → {"results":[{"id":"process:1","portalId":"process-center","title":"IT 서비스",...}],"partial":false,"unavailablePortals":[],"catalogStale":false} |
| 경로 | 게이트웨이 → DS /api/directory/v1/control-plane/catalog → 포털 어댑터(http://localhost:3000/api/explore/v1/search) → ProcessCenter DB 검색 |
검증 중 확인한 주의점
- Control plane 인증 scope:
portalRoutes.ts가requirePortalScopes("directory:read")로 보호한다. 관리자 API key는 401이므로directory:read가 있는 포털 API key(개발 스택의dsvc_…)를 사용해야 한다. - 포털 baseUrl이
localhost:portal_configurations.base_url이 브라우저 기준(http://localhost:3000)이므로, 게이트웨이 컨테이너는 loopback 전달자(dev-loopback-forward.mjs 3000 process-center 3000)로 접근한다(DB 수정 불필요). - catalog 조회 실패는 500으로 표면화된다(캐시가 없을 때). 운영에서는
CONTROL_PLANE_API_KEY·주소를 먼저 점검한다. - PowerShell에서 한글 쿼리를 보낼 때 인코딩이 깨질 수 있다(ASCII 쿼리로 검증 권장). 서버·DB 자체의 한글 처리는 정상이다.
6. 미해결
| # | 항목 | 결정 필요 |
|---|---|---|
| 1 | 게이트웨이 사용자 인증 방식 — DS 사용자 토큰 직접 검증 vs 위임 토큰 교환 | 위임 토큰 계약 D0와 함께 결정 |
| 2 | 포털 서비스 토큰 저장 위치 — env vs DS 키 원장 | 운영 보안 수준 |
| 3 | 탐색 UI 노출 — 각 포털 임베드 vs 공통 셸 | portal-foundation과 합의 |
| 4 | 배포 형태 — stateless HTTP 서비스(권장) vs 워커 | 배포 환경 |
| 5 | timeoutMs 기본값 합의(현재 1500ms, 포털별 조정 가능) | 운영 정책 |