Skip to content

연계 계약 (integration-contract) ​

설계서 §9(프로세스 포탈 연계)·§10(내부 API)의 계약과, portal-foundation 재사용 매핑을 정리한다.

중요: §9의 계약은 개발용 제안이다. 기존 프로세스 포탈이 실제로 아래 API나 필드를 제공한다고 가정하지 않는다(설계서 §9 서두). 실제 제공 여부는 §17에서 확정한다.

1. 프로세스 포탈 어댑터 (§9.1) ​

코드 위치: shared/processPortal.ts (계약) · server/adapters/process-portal/mock.ts (Mock) · server/adapters/process-portal/index.ts (팩토리)

ts
type NodeKey = { sourceSystemId: string; taxonomyId: string; externalNodeId: string };

interface ProcessPortalAdapter {
  readonly kind: "mock" | "contract";
  listNodes(input: { taxonomyId: string; cursor?: string; changedSince?: string }):
    Promise<{ nodes: ProcessNode[]; nextCursor?: string; snapshotToken?: string; complete: boolean }>;
  getNode(key: NodeKey): Promise<ProcessNode | null>;
  checkAccess(input: { subjectId: string; keys: NodeKey[]; action: "VIEW" | "REVIEW" }):
    Promise<{ decisions: Array<NodeKey & { allowed: boolean }>; validUntil: string }>;
}
원칙구현
원본은 읽기 전용 (§2.1·§2.4)어댑터에 변경 메서드를 두지 않는다
동일 이름을 같은 노드로 취급하지 않음 (§7.1)복합키 (sourceSystemId, taxonomyId, externalNodeId)
부분 응답으로 삭제 판정 금지 (§9.2)complete: false 이면 누락을 삭제로 보지 않는다
권한 캐시 최대 5분 (§4.2)checkAccess.validUntil
명칭만 바뀐 경우 승인 유지 (§9.3)materialFingerprint() 로 실질 필드만 비교
노드 권한 없는 매핑은 '접근 제한' 표시 (§4.2)목록·집계에도 동일 권한 필터 적용

오류 분류 (§10.5 정합) ​

UNAUTHENTICATED(401/403) · INVALID_REQUEST(400) · UNAVAILABLE(5xx·타임아웃, 재시도 대상) · UNEXPECTED(형식 오류)

2. 동기화·변경 영향 (§9.2·§9.3) ​

항목계약
최초커서 기반 전체 수집
증분기본 15분 주기(PROCESS_SYNC_INTERVAL_MS), 변경분 API 없으면 스냅샷 비교
전체 대조하루 1회 제안(PROCESS_FULL_RECONCILE_INTERVAL_MS)
커서 확정전체 처리 성공 후에만 lastSuccessCursor 갱신
재실행같은 데이터로 중복 생성·중복 알림이 없어야 함(dedupeKey)
404 / 권한 거부 / 일시 장애구분한다. 권한 거부를 노드 삭제로 처리하지 않는다
캐시 신선도24시간 미갱신 시 STALE(제안)
게시 시대상 노드를 원본에서 재검증, 실패 시 전이 보류 + 503

변경 판정(§9.3): 명칭·표기 정정 → 이력만 기록 / 레벨·부모·경로·업무 정의·비활성 변경 → REVIEW_REQUIRED / 폐기 → SOURCE_RETIRED / 확정 삭제·전체 대조 누락 → SOURCE_MISSING / 분할·통합 → 후속 후보 표시(자동 재매핑 금지).

2. 상대측이 구현해야 할 REST 계약 (§17 확정 대상) ​

AgentCenter 는 아래 3개 경로를 호출합니다(server/adapters/process-portal/processCenter.ts).

상대측 구현: ProcessCenter 가 읽기 전용으로 제공합니다(server/remoteProcessNodes.ts). 자체 DB + tRPC 에 더해 서비스 자격증명으로 호출하는 REST 3경로를 노출하며, 원본 노드를 생성·수정하지 않습니다(§2.1·§2.4).

  • 인증: Authorization: Bearer <AGENT_CENTER_API_KEY> — 키는 배포 시 양쪽에 주입합니다(ProcessCenter 가 발급·보관).
  • taxonomyId 는 L1(루트) 노드의 code 입니다(예: IT-VC). 어떤 L1 을 어느 분류체계로 볼지는 운영 값 확정이 필요합니다(§17).
  • 포털에 노출하는 상태는 ACTIVE·INACTIVE·RETIRED 이고, 작성 중(DRAFT)은 RACI 담당자·관리자에게만 열립니다.
  • successorNodeIds 는 흐름 그래프(task_flow_edges) 연계 전까지 빈 배열입니다.

남은 §17 항목은 위 taxonomyId 운영 값 확정과 서비스 키 발급입니다.

계약 v2 전환 완료 (2026-09-20): 노드의 표준 식별자를 내부 대리 키 → 업무 코드(전역 유일)로 바꾸고 게시본만 노출하는 v2 로 전환했습니다. NodeKey 는 { sourceSystemId, taxonomyCode, nodeCode } 이고 ProcessNode 필드도 v2 와 같습니다(parentNodeCode·pathCodes·sourceVersion = 게시본 버전). 시스템 목록도 v2(/api/v2/systems)가 함께 공급합니다. 계약: ProcessCenter/docs/PROCESS_REFERENCE_CONTRACT.md, 공통 규약: portal-foundation docs/REFERENCE_DATA_SUPPLY_CONVENTION.md. 소비자 측 규칙: 참조 대상이 RETIRED 이고 승계가 있으면 승계를 따르고, 없으면 무효로 표시합니다. 업무 코드 별칭(businessCode)은 표시용입니다 — code(노드 코드)가 표준 식별자이고, 별칭으로 노드를 참조하지 않습니다(ProcessCenter docs/PROCESS_NODE_GOVERNANCE_PLAN.md §9.4). 배포 시 소비자 키를 ProcessCenter PROCESS_NODE_CONSUMERS_JSON 에 두 스코프(process:read·process:access)로 등록해야 합니다 — 스코프가 없으면 권한 판정 호출이 403 입니다.

메서드경로요청응답
GET/api/v2/process-nodes쿼리 taxonomyCode(필수), cursor, changedSince{ nodes: ProcessNode[], nextCursor?, snapshotToken, complete: boolean }
GET/api/v2/process-nodes/{taxonomyCode}:{nodeCode}—ProcessNode 또는 404
POST/api/v2/process-node-access{ subjectId, keys: NodeKey[], action: "VIEW" | "REVIEW" }{ decisions: (NodeKey & { allowed: boolean })[], validUntil: string }

인증: Authorization: Bearer <서비스 자격증명> — 소비자별 키이며, 목록·resolve 는 process:read, 권한 판정은 process:access 스코프가 필요합니다(ProcessCenter docs/PROCESS_REFERENCE_CONTRACT.md §3.6).

상대측이 지켜야 하는 것:

  1. complete 를 정확히 표시 — true 가 아니면 부분 응답으로 취급되어 삭제 판정에 쓰이지 않습니다(§9.2).
  2. 404 와 권한 거부를 구분 — 권한 거부를 404(노드 삭제)로 처리하면 매핑이 잘못 폐기됩니다.
  3. 권한 응답은 요청한 키를 모두 포함 — 빠진 키는 거부로 채워집니다(§4.2).
  4. validUntil 제공 권장 — 없으면 60초로 짧게 잡습니다(§4.2 최대 5분).
  5. 오류 상태 코드 의미 유지 — 401/403 = 인증·권한 문제(재시도 없음), 5xx·429 = 일시 장애(재시도), 400/422 = 요청 오류.

NodeKey = { sourceSystemId, taxonomyId, externalNodeId }, ProcessNode = NodeKey & { name, description, levelCode, parentExternalNodeId, pathIds[], sourceVersion, taxonomyVersion, status, successorNodeIds[], updatedAt, sourceUrl? } — 전체 정의는 shared/processPortal.ts.

상대 미구현 상태에서의 동작: PROCESS_ADAPTER=contract 로 기동은 하며, 프로세스 조회 시점에 503 PROCESS_SOURCE_UNAVAILABLE(§9.4)로 드러납니다. 게시·검토 전이가 조용히 통과하지 않습니다.

2-1. 중앙(콘솔) 공급 — 공통 관리자 메뉴 (규약 §1 · 설계 §3) ​

관리자 셸의 공통(FOUNDATION) 구간은 포털이 소유하지 않습니다 — 중앙(PortalConsole)이 공급합니다. 포털(PORTAL) 구간은 이 경로의 대상이 아닙니다.

http
GET {PORTAL_CONSOLE_ORIGIN}/api/directory/v1/menu?portalId=agent-center
Authorization: Bearer <PORTAL_CONSOLE_API_KEY>     # scope: directory:read
→ { data: { portalId, version, items: [...] }, meta: { requestId, generatedAt } }
  • version 은 내용 해시입니다(콘솔에 저장소가 없어 updatedAt 정본이 아닙니다). 항목이 바뀌면 값이 바뀝니다.
  • 콘솔 측 계약 전문: PortalConsole/docs/portal-admin-menu-supply.md.
  • 서버 소비 코드: server/adapters/identity/consoleAdminMenu.ts, 계약·폴백 시드: shared/adminMenu.ts, 클라이언트 노출: trpc.system.adminMenu(인증 필요).

폴백 순서(어느 단계에서도 빈 관리자 메뉴를 만들지 않습니다): 중앙 응답 → 마지막 정상 캐시(TTL 기본 15분) → 포털 로컬 시드. PORTAL_CONSOLE_API_KEY 가 없으면 중앙 공급을 쓰지 않고 로컬 시드로 동작합니다(오류가 아니라 구성 상태).

중앙이 응답했는데 빈 목록이면 그대로 반영합니다 — 공급 주체가 중앙이므로 포털이 항목을 되살리지 않습니다. 로컬 시드는 콘솔에 닿지 못할 때의 안전판입니다.

셸에 넣을 수 없는 항목은 서버가 버립니다: visibility ≠ ADMIN, enabled=false, owner ≠ FOUNDATION, type ≠ LINK, / 로 시작하지 않는 경로.

2-2. 통합 탐색 어댑터 — 게이트웨이가 이 포털을 부르는 계약 (§8.2) ​

ExploreGateway 가 포털 쪽으로 호출하는 방향입니다(§2 의 프로세스 어댑터와 반대). 계약 원문은 ExploreGateway/docs/adapter-onboarding.md §3, 선행 구현은 ProcessCenter server/remoteExplore.ts.

http
POST {APP_ORIGIN}/api/explore/v1/search
Authorization: Bearer <EXPLORE_SERVICE_TOKEN>     # 불일치 401 · 미설정 503
x-explore-subject-id: <사용자 식별자>              # 필수 — 없으면 400
x-explore-org-id: <중앙 부서 식별자>               # 선택 — 없으면 ORG 범위 항목은 결과에서 빠진다
x-explore-roles: VIEWER,ADMIN                     # 선택 — 없으면 VIEWER(최소 권한)
x-explore-request-id: <상관관계 ID>                # 선택 — 응답 requestId 로 에코된다
{ "query": "런북", "limit": 20 }                   # query 1~200자 · limit 1~30(초과분은 자른다)
  • 응답은 { results: [{ id, kind, portalId, portalLabel, title, summary?, meta?, href? }], partial, unavailablePortals, requestId } — portal-foundation PortalExploreSearchResponse 와 같은 형태라 게이트웨이가 정규화할 것이 없습니다. portalId 는 카탈로그에 등록된 agent-center 이고 게이트웨이가 자기검증으로 이 값만 남깁니다.
  • 권한은 포털이 최종 적용합니다(§4.2): 색인을 만들기 전에 filterVisibleResources(Agent) · canSeeMenu(메뉴·매뉴얼)를 통과시켜, 볼 수 없는 것은 개수·제목조차 응답에 실리지 않습니다. 게이트웨이는 권한을 대체하지 않습니다.
  • 주체·역할 헤더를 받는 근거는 서비스 토큰을 가진 호출자만 통과한다는 것입니다(토큰이 신뢰 경계). 반대로 주체를 추정하지는 않습니다 — 없으면 400 입니다.
  • 서버 코드: server/adapters/explore/route.ts(어댑터) · server/adapters/explore/index.ts(색인·필터·순위 — 셸 검색과 같은 함수). 셸 검색은 tRPC explore.search(server/routers/explore.ts)로 같은 색인을 씁니다.
  • 카탈로그 등록(운영 설정): PortalConsole 의 portal_configurations 에 agent-center(enabled=true · applicationType=CONSUMER_PORTAL · 게이트웨이에서 닿는 baseUrl)가 있어야 하고, 기본 정책이 이미 이 경로와 같으므로 PORTAL_EXPLORE_CATALOG_JSON 은 기본값을 그대로 써도 됩니다({ "capabilities": ["search"], "endpoint": "/api/explore/v1/search", "timeoutMs": 1500 }). 게이트웨이에는 PORTAL_SERVICE_TOKENS_JSON = {"agent-center": "<EXPLORE_SERVICE_TOKEN 과 같은 값>"} — 토큰이 없는 포털은 게이트웨이가 어댑터를 구성하지 않습니다.
  • 확인 절차: docs/runbook.md §2-2.

2-3. 근거 기반 답변 — 이 포털이 ModelGateway 를 부르는 계약 (§2-7) ​

셸 통합 탐색의 답변은 검색과 다른 호출이다(검색은 즉시, 답변은 문서 독해 뒤). 포털 서버가 색인 상위 근거를 모델 게이트웨이로 요약하고 인용을 붙인다 — 게이트웨이는 색인을 소유하지 않는다(규약 EXPLORE_ANSWER_CONVENTION.md §2-7).

  • 코드: server/adapters/model-gateway/index.ts(HTTP·귀속 헤더) · server/application/exploreAnswerService.ts(근거 선택·프롬프트·인용)
  • 호출: POST {MODEL_GATEWAY_URL}/v1/chat/completions (OpenAI 호환 · 비스트리밍 — 게이트웨이 스트리밍 미지원)
  • 인증: Authorization: Bearer <MODEL_GATEWAY_SERVICE_TOKEN> — 서버 간 전용이며 브라우저에 내려가지 않는다
  • 귀속 헤더: x-portal-id: agent-center · x-purpose: explore-answer · x-actor: <subjectId> · x-organization-id: <orgId> (없는 값은 보내지 않는다 — 게이트웨이가 미분류로 기록한다)
  • 본문: { model, messages: [system, user], max_tokens: 500 } — 근거는 권한 필터(§4.2)를 지난 상위 5건(문서 우선)뿐이다
  • 오류: 401(토큰 불일치) · 429(쿼터) · 502·503·504(업스트림) → 포털은 503 EXPLORE_ANSWER_UNAVAILABLE 로 올린다. 빈 답변·빈 결과로 위장하지 않는다(§10.5)
  • 미구성(AI_ENABLED=false 또는 값 누락): 호출하지 않고 셸도 질문 슬롯을 켜지 않는다 — explore.capabilities.answer=false
규칙구현
근거 없는 답변 금지 (규약 §2-4)권한을 통과한 근거가 0건이면 모델을 부르지 않는다(reason=NO_EVIDENCE)
인용 없는 답변 금지인용은 프롬프트에 넣은 근거에서만 만든다 — 모델이 말한 출처를 옮기지 않는다
권한 범위 밖 유출 금지 (§4.2)프롬프트·인용 모두 searchExplore 결과에서만 나온다 — 가려진 문서는 문장으로도 새지 않는다
비밀 노출 금지 (§14.1)서비스 토큰·시스템 프롬프트는 서버에만 있고 화면에는 구성 여부만 간다
  • 확인 절차: docs/runbook.md §2-3.
  • 운영 등록: x-purpose 매핑·쿼터·허용 모델은 ModelGateway 관리자 API(/admin/api)가 소유한다(PortalConsole 콘솔의 「모델 게이트웨이」 화면). 포털 쪽은 구성 여부만 관리자 「연계·동기화」 화면에 표시한다.

3. portal-foundation 재사용 매핑 ​

3-1. 화면 → 컴포넌트 (§8.2) ​

화면 ID화면portal-foundation
UI-01현황PortalMetricGrid, PortalPanel, PortalPage, PortalStatusBadge
UI-02카탈로그PortalDataTable, PortalToolbar, PortalTextField, PortalSelect/PortalMultiSelect
UI-03Agent 상세PortalDescriptions, PortalTabs, PortalPageHeader
UI-04등록·편집PortalTextField, PortalTextArea, PortalSelect, PortalDialog, PortalTabs
UI-05매핑 편집PortalTree(읽기 전용·원본 트리), PortalDialog, PortalSelect
UI-06프로세스별 AgentPortalTree, PortalDataTable, PortalStack, PortalPanel
UI-07검토 상세PortalDescriptions, PortalConfirm, PortalAiSummaryCard(차이 요약)
UI-08변경 영향PortalDataTable, PortalStatusBadge
UI-09버전 비교PortalDescriptions, PortalTabs, PortalStack
UI-10연계 관리PortalDataTable, PortalStatusBadge(FRESH/STALE/UNAVAILABLE), PortalToolbar
UI-11감사 이력PortalDataTable, PortalToolbar, PortalTextField
공통 셸§8.1 메뉴PortalShell, PortalAdminShell, PortalBreadcrumbs
상태·오류·빈 화면§8.5PortalLoadingState, PortalErrorState, PortalEmptyState, NetworkErrorBoundary
알림§7.7PortalNotificationCenter
인증SSO 지점AuthCard

3-2. 도메인 요구 → 패키지 ​

설계서재사용 패키지
감사 이벤트 audit_event (§7.7·§14.1)@hohyun94/portal-security (createAuditEvent 등)
scope·서버 권한 검증 (§4.x·§10.1)@hohyun94/portal-security
알림 dedupe (§7.7)@hohyun94/portal-operations
동기화·영향 Worker 작업 상태 (§9.2·§13.2)@hohyun94/portal-operations (QUEUED→RUNNING→SUCCEEDED/FAILED/CANCELLED)
검색 정규화·허용 정렬 키 (§10.2)@hohyun94/portal-runtime (normalizeSearchQuery)
기능 플래그·헬스·관측 (§14.x)@hohyun94/portal-runtime
후속 AI 추천 (§11.2)@hohyun94/portal-ai (completeStructured·extractJsonPayload·PortalAiProvider·createAiResponseCache)
지식 원천 knowledge_refs (§7.4)@hohyun94/portal-ai 온톨로지(approvedConcepts·approvedRelations·OntologyTermPack.packVersion)
사용자·조직 참조 (§2.1)@hohyun94/directory-sync

현재 vendor/ 에는 portal-foundation(0.6.158) · portal-ai(0.1.0) · portal-operations(0.3.0) 만 두었다. portal-security·portal-runtime·directory-sync 는 각각 감사·권한, 검색·플래그, 조직 동기화 단계에서 추가한다.

portal-foundation tarball 은 로컬에서 pnpm --dir packages/ui-react pack 으로 만든다(게시본과 같은 내용). 새 버전을 쓰려면 portal-foundation 에서 버전 범프 → 태그 게시 → vendor/ 갱신 순서로 진행한다. 0.6.158 부터 셸 검색창이 실제 제출(PortalExploreDialog.onSubmit)과 답변 슬롯(onExploreAsk)을 받는다 — 0.6.157 은 onSearchSubmit 을 선언만 하고 다이얼로그로 넘기지 않아 제출이 동작하지 않았다(D-070).

4. portal-foundation 측 보완 필요 (gap) ​

#항목처리
1AgentCenterMark (제품 마크) 없음portal-foundation 후속 PR (D-003)
2스텝 마법사(등록 6단계, §8.3)antd Steps 조합 또는 포탈 도메인 컴포넌트. 반복 요구 시 공통 승격
3구조화 버전 비교(UI-09)포탈 도메인 컴포넌트로 구현

5. 외부 제공 연계 (§9.5) ​

MVP에서 제공: Agent 상세 URL, 프로세스별 목록 URL. 조회 API(제안): GET /api/v1/process-nodes/{nodeRefId}/agents?includeDescendants=false 서버 간 호출도 호출 주체와 최종 사용자의 권한을 확인해야 하며, 서비스 계정이라도 모든 노드 정보를 반환하지 않는다.