Skip to content

어댑터 온보딩 · 런타임 준비 상태 ​

작성 시각: 2026-09-13 · 상대 저장소 실사 기반(PortalConsole, ProcessCenter) 관련 계약: docs/control-plane-contract.md · docs/SERVICE_BOUNDARIES.md

1. 현황 요약 ​

구현됨(라이브러리)

구성위치내용
계약 타입src/control-plane.tsCONTRACT_VERSION="1", EXPLORE_CAPABILITIES=["search","answer"], catalog 응답 검증
Control plane 클라이언트src/control-plane.tsGET {origin}/api/directory/v1/control-plane/catalog(Bearer API key, 기본 1.5s), 계약 위반 시 오류
Adapter registrysrc/registry.tsregister()(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.tsresults·partial·unavailablePortals·requestId (소비 포털의 기존 계약과 동일)
테스트src/*.test.tscontrol plane·registry·http adapter

구현됨(런타임, 2026-09-13) ✔

구성위치내용
HTTP APIsrc/server.tsGET /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.tsPortalConsole 사용자 토큰 검증(HS256, iss=directory-service, aud=directory-user) → sub를 subjectId로 어댑터에 전달
Composition rootsrc/runtime.tscatalog 조회 → 포털별 어댑터 자동 구성(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 능력을 선언한 서비스(예: RagService rag-center). 그 외는 applications[] 에만 들어간다 — 게이트웨이의 answer provider 는 포털이 아닐 수 있다(§3-1).
  • answer 능력을 선언한 항목은 explore.answerEndpoint(기본 /api/explore/v1/ask)를 함께 광고한다.

ProcessCenter 등록 예시(운영 설정)

jsonc
// 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 등록 예시(운영 설정)

jsonc
// 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)로 결과에 합류한다. 게이트웨이는 답을 만들지 않는다(오케스트레이션만).
  • 소비 포털은 게이트웨이 우선, 자기 색인 폴백을 쓴다(구현 선례):
    포털답변 절차폴백
    ProcessCenterexplore.gatewayAsk (server/exploreGatewaySearch.ts)로컬 RAG(answerPrivateRag) — DISABLE_LOCAL_RAG_FALLBACK=true 로 끌 수 있다
    AgentCenterexplore.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 검색

검증 중 확인한 주의점

  1. Control plane 인증 scope: portalRoutes.ts가 requirePortalScopes("directory:read")로 보호한다. 관리자 API key는 401이므로 directory:read가 있는 포털 API key(개발 스택의 dsvc_…)를 사용해야 한다.
  2. 포털 baseUrl이 localhost: portal_configurations.base_url이 브라우저 기준(http://localhost:3000)이므로, 게이트웨이 컨테이너는 loopback 전달자(dev-loopback-forward.mjs 3000 process-center 3000)로 접근한다(DB 수정 불필요).
  3. catalog 조회 실패는 500으로 표면화된다(캐시가 없을 때). 운영에서는 CONTROL_PLANE_API_KEY·주소를 먼저 점검한다.
  4. PowerShell에서 한글 쿼리를 보낼 때 인코딩이 깨질 수 있다(ASCII 쿼리로 검증 권장). 서버·DB 자체의 한글 처리는 정상이다.

6. 미해결 ​

#항목결정 필요
1게이트웨이 사용자 인증 방식 — DS 사용자 토큰 직접 검증 vs 위임 토큰 교환위임 토큰 계약 D0와 함께 결정
2포털 서비스 토큰 저장 위치 — env vs DS 키 원장운영 보안 수준
3탐색 UI 노출 — 각 포털 임베드 vs 공통 셸portal-foundation과 합의
4배포 형태 — stateless HTTP 서비스(권장) vs 워커배포 환경
5timeoutMs 기본값 합의(현재 1500ms, 포털별 조정 가능)운영 정책