연계 계약 (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 (팩토리)
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-foundationdocs/REFERENCE_DATA_SUPPLY_CONVENTION.md. 소비자 측 규칙: 참조 대상이RETIRED이고 승계가 있으면 승계를 따르고, 없으면 무효로 표시합니다. 업무 코드 별칭(businessCode)은 표시용입니다 —code(노드 코드)가 표준 식별자이고, 별칭으로 노드를 참조하지 않습니다(ProcessCenterdocs/PROCESS_NODE_GOVERNANCE_PLAN.md§9.4). 배포 시 소비자 키를 ProcessCenterPROCESS_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).
상대측이 지켜야 하는 것:
complete를 정확히 표시 —true가 아니면 부분 응답으로 취급되어 삭제 판정에 쓰이지 않습니다(§9.2).- 404 와 권한 거부를 구분 — 권한 거부를 404(노드 삭제)로 처리하면 매핑이 잘못 폐기됩니다.
- 권한 응답은 요청한 키를 모두 포함 — 빠진 키는 거부로 채워집니다(§4.2).
validUntil제공 권장 — 없으면 60초로 짧게 잡습니다(§4.2 최대 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) 구간은 이 경로의 대상이 아닙니다.
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.
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-foundationPortalExploreSearchResponse와 같은 형태라 게이트웨이가 정규화할 것이 없습니다.portalId는 카탈로그에 등록된agent-center이고 게이트웨이가 자기검증으로 이 값만 남깁니다. - 권한은 포털이 최종 적용합니다(§4.2): 색인을 만들기 전에
filterVisibleResources(Agent) ·canSeeMenu(메뉴·매뉴얼)를 통과시켜, 볼 수 없는 것은 개수·제목조차 응답에 실리지 않습니다. 게이트웨이는 권한을 대체하지 않습니다. - 주체·역할 헤더를 받는 근거는 서비스 토큰을 가진 호출자만 통과한다는 것입니다(토큰이 신뢰 경계). 반대로 주체를 추정하지는 않습니다 — 없으면 400 입니다.
- 서버 코드:
server/adapters/explore/route.ts(어댑터) ·server/adapters/explore/index.ts(색인·필터·순위 — 셸 검색과 같은 함수). 셸 검색은 tRPCexplore.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-03 | Agent 상세 | PortalDescriptions, PortalTabs, PortalPageHeader |
| UI-04 | 등록·편집 | PortalTextField, PortalTextArea, PortalSelect, PortalDialog, PortalTabs |
| UI-05 | 매핑 편집 | PortalTree(읽기 전용·원본 트리), PortalDialog, PortalSelect |
| UI-06 | 프로세스별 Agent | PortalTree, 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.5 | PortalLoadingState, PortalErrorState, PortalEmptyState, NetworkErrorBoundary |
| 알림 | §7.7 | PortalNotificationCenter |
| 인증 | 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-foundationtarball 은 로컬에서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)
| # | 항목 | 처리 |
|---|---|---|
| 1 | AgentCenterMark (제품 마크) 없음 | 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 서버 간 호출도 호출 주체와 최종 사용자의 권한을 확인해야 하며, 서비스 계정이라도 모든 노드 정보를 반환하지 않는다.