# 설계서: PoC4 MCP AI Console 역설계 및 VPD/FGA 감사 운영 현행화 (#654) > **상태**: Implemented snapshot documented · follow-up contract pending > **작성**: [AI] Architect · **최종수정**: 2026-07-14 > **추적성** — Redmine: #654 · 관련 ADR: 없음 · 구현 커밋: `5bdd242`, `33399f9`, `1f6d974` > · 스냅샷: `poc4_active_source_20260714/` · 원본 archive: `poc4_active_source_20260714/poc4_active_source_20260714.tar.gz` · 현행 구현 파일: `apps/poc4/mcp_discovery_ui.py`, `src/mcp_tool_router.py`, `src/oci_genai_sdk.py`, `src/poc3/model_registry.py`, `config/mcp_servers.json` · 테스트: Python 3.11 `py_compile`, 기존 백오피스 `mvn -q test` ## 1. 목적 (Why) PoC4 MCP AI Console이 KB VPD MCP를 사용자별 Bearer 토큰으로 안전하게 호출하면서도, 환경별 URL·전송 방식·오류 처리를 하나의 명확한 계약으로 관리한다. 현재 UI의 기본 호출 흐름은 동작한다. 다만 `kb_mcp` 설정에는 `endpoint_url`과 `base_url_env`가 함께 있고, UI는 `endpoint_url`을 우선 사용한다. 따라서 `KB_MCP_BASE_URL`을 바꿔도 실제 호출 대상이 바뀌지 않는다. 또한 `custom_python`은 로컬 8500 MCP용 이름이므로 외부 KB VPD MCP의 통신 계약을 설명하지 못한다. ## 1.1 현행 구현 역설계 요약 이번 문서는 목표 설계를 먼저 정한 것이 아니라, 배포 서버에서 이미 동작하던 PoC4 MCP AI Console 스냅샷을 저장소로 가져온 뒤 구현을 역으로 읽어 작성했다. 따라서 아래 표가 현재 동작의 기준이다. | 영역 | 현행 구현 | |---|---| | 저장소 위치 | `poc4_active_source_20260714/` 단일 폴더에 격리 | | 실행 진입점 | 직접 실행은 `streamlit run apps/poc4/mcp_discovery_ui.py --server.address 0.0.0.0 --server.port 8622` | | Python 런타임 | Python 3.11 이상. 배포 서버 검증 런타임은 `/home/opc/poc_4/.python-runtime/cpython-3.11.15+20260610/bin/python3.11` | | Streamlit 화면 | `apps/poc4/mcp_discovery_ui.py` 단일 대형 UI. KB 테마, 포털 로그인, 대화 이력, MCP discovery/call, evidence 수집, 답변 합성을 포함 | | MCP registry | `config/mcp_servers.json`. `kb_mcp`와 `kb_vector_mcp` 두 서버를 선언 | | KB 정형 MCP | `https://kb.cloud-handson.com/mcp`, 기본 tool `ords.query.kb_select_ai_vpd` | | KB 벡터 MCP | `http://127.0.0.1:9978/mcp`, allowlist `hybrid_rerank_search` | | 토큰 preset | `config/vpd_token_presets.json`. 실제 토큰 원문은 포함하지 않고 placeholder만 둠 | | 대화 저장소 | 기본 `data/poc4_mcp_chat.sqlite3`, 환경변수 `POC4_CHAT_DB_PATH`로 변경 가능 | | DB evidence 연결 | 기본 env file `/home/opc/kbmcp/.env`, wallet fallback `/home/opc/wallet/kbaipoc` | | OCI GenAI | `OCI_AUTH_TYPE=config_file`, `~/.oci/config`, `DEFAULT` profile 기반. model profile은 `config/poc3_model_profiles.json`에서 로드 | | 보안 원칙 | wallet, DB password, wallet password, 실제 VPD token, 대화 DB는 저장소에 포함하지 않음 | 현행 데이터 흐름은 다음과 같다. ```text Streamlit 사용자 로그인 ↓ VPD 사용자 preset 또는 직접 Bearer token 선택 ↓ config/mcp_servers.json 로드 ↓ 각 MCP 서버에 initialize → initialized notification → tools/list ↓ 질문 유형에 따라 single 또는 agent 실행 계획 선택 ↓ tools/call 호출 - Authorization: Bearer <현재 VPD token> - JSON-RPC body에는 tool name과 arguments만 포함 ↓ 정형 MCP / 벡터 MCP 결과 수집 ↓ 필요 시 /home/opc/kbmcp/.env + /home/opc/wallet/kbaipoc 로 Oracle evidence 조회 ↓ OCI GenAI로 최종 답변 합성 ↓ SQLite 대화 이력 저장 ``` ## 1.2 현행 구현 컴포넌트 | 파일 | 책임 | |---|---| | `apps/poc4/mcp_discovery_ui.py` | Streamlit 화면, 포털 로그인, MCP discovery/call, agent loop, evidence 수집, 답변 합성, 대화 이력 저장 | | `apps/poc4/ui_theme.py` | UI theme 보조 코드 | | `src/mcp_tool_router.py` | 발견된 MCP tool descriptor를 기반으로 LLM router가 server/tool을 선택하고 tool arguments를 구성 | | `src/oci_genai_sdk.py` | OCI Generative AI 호출 경계. MCP/VPD token을 알지 않는 최소 completion client | | `src/poc3/model_registry.py` | 모델 profile registry 로드, region/endpoint 해석, 환경 override 처리 | | `src/poc3/questions.py` | 데모 질문 목록 | | `config/mcp_servers.json` | MCP 서버 registry. 현재는 `endpoint_url`이 있으면 이를 우선 사용 | | `config/poc3_model_profiles.json` | `gpt55_oci`, `gpt54_mini_oci`, `grok43`, `llama4_maverick`, `llama33_70b` profile | | `config/vpd_token_presets.json` | 데모 사용자 token preset 구조. 실제 토큰은 배포 환경에서 교체 | | `scripts/poc4/start_8622_langgraph_tc_ui_nohup.sh` | 원본 PoC4 런처 wrapper. 현재 스냅샷의 직접 entrypoint와는 다르게 `/home/opc/poc_4/scripts/poc4/run_8622_langgraph_tc_ui.sh` 및 `apps/poc4/langgraph_tc_ui.py`를 참조 | 런처 주의사항: 스냅샷에는 `mcp_discovery_ui.py` 직접 실행에 필요한 소스가 들어 있지만, 포함된 `start_8622...` script는 원본 서버의 공용 runtime wrapper에 의존한다. 이 저장소에서 독립 실행하려면 `SOURCE_README.md`의 직접 `streamlit run` 명령을 사용하거나, 별도 wrapper를 작성해야 한다. ## 1.3 현행 보안/비밀정보 경계 - `VpdTokenPreset.token`은 dataclass에서 `repr=False`이며, UI는 token을 정규화한 뒤 Authorization header에만 넣는다. - `_NoRedirectHandler`는 redirect 시 Authorization header가 다른 endpoint로 전달되는 것을 막는다. - `src/mcp_tool_router.py`의 router는 질문과 tool descriptor만 받으며 bearer token 또는 provider credential을 받지 않는다. - `src/oci_genai_sdk.py`는 `.env`에서 `OCI_AUTH_TYPE`, `OCI_CONFIG_FILE`, `OCI_GENAI_COMPARTMENT_ID`, `OCI_PROFILE`만 읽는다. - Oracle audit/business evidence 조회는 `/home/opc/kbmcp/.env`에서 `ORACLE_DB_USER`, `ORACLE_DB_PASSWORD`, `ORACLE_DSN`, `ORACLE_WALLET_PASSWORD`, `ORACLE_WALLET_DIR`를 읽는다. - wallet 기본 fallback은 `/home/opc/wallet/kbaipoc`이다. - 저장소에는 실제 `.env`, wallet, SQLite 대화 DB, 실제 VPD bearer token 원문을 포함하지 않는다. ## 1.4 현행 구현과 후속 목표의 차이 | 항목 | 현행 구현 | 후속 목표 | |---|---|---| | KB MCP provider/transport 명칭 | `provider=custom_python`, `transport=http` | `kb_vpd_streamable_http`, `streamable_http`처럼 외부 KB VPD MCP 계약을 명확히 표현 | | endpoint 해석 | `endpoint_url`이 있으면 이를 우선 사용하고, 없으면 env 값을 사용 | KB MCP는 `KB_MCP_BASE_URL + /mcp` 단일 계약으로 해석 | | tool routing | route가 여러 개면 LLM router/agent mode를 사용할 수 있음 | KB 단일 tool 호출에서는 router 없이 direct 선택 | | 401/403 표시 | 현재 둘 다 “MCP 인증에 실패” 계열 안전 메시지로 축약 | 토큰 없음/만료/권한 부족을 UI에서 명확히 분리 | | start script | 원본 `/home/opc/poc_4` runtime wrapper 의존 | 스냅샷 폴더 안에서 독립 실행 가능한 wrapper 추가 | | 회귀 테스트 | 원본 서버 Python 3.11 `py_compile`, Java `mvn -q test` | PoC4 전용 단위 테스트와 실제 MCP HTTP smoke test 추가 | ## 2. 범위 (Scope) - **포함**: - `apps/poc4/mcp_discovery_ui.py`의 KB MCP endpoint, 인증 헤더, JSON-RPC, 오류 처리 정비 - `config/mcp_servers.json` 및 sample의 KB MCP 선언 정비 - KB MCP의 단일 도구 `ords.query.kb_select_ai_vpd` 호출 계약 문서화 - VPD Backoffice `/mcp`과의 HTTP 상태·프로토콜 버전 호환성 점검 및 필요한 최소 보완 - **제외 (out of scope)**: - 기존 `kb_vector_mcp` 및 `custom_python` 8500 RAG MCP의 변경 - VPD 권한 규칙, Select AI SQL 생성 규칙, ORDS 비즈니스 로직 변경 - OAuth Authorization Server, 사용자 로그인/토큰 발급 UI 재설계 - VPD 토큰 원문을 환경변수·레지스트리에 저장하는 방식 ## 3. 인수조건 (Acceptance Criteria) 아래 체크박스는 최종 목표 기준이다. 2026-07-14 스냅샷 반입 상태에서는 일부만 충족한다. - [ ] KB MCP URL은 `KB_MCP_BASE_URL` 하나에서만 해석되고 `/mcp` path가 안전하게 결합된다. - [ ] `initialize`, `notifications/initialized`, `tools/list`, `tools/call`의 모든 HTTP 요청에 현재 선택된 사용자의 `Authorization: Bearer `만 전송된다. - [ ] Bearer 토큰은 JSON-RPC arguments, SQLite 대화 이력, UI 결과, 애플리케이션 로그에 저장·출력되지 않는다. - [ ] 호출 가능한 도구는 `ords.query.kb_select_ai_vpd` 하나이며, 인자는 `prompt`와 `limit`만 허용된다. - [ ] 단일 도구 구성에서는 LLM router를 호출하지 않고 허용 도구를 직접 선택한다. - [ ] 토큰 없음/위조/만료는 HTTP `401`, 유효 토큰의 권한 부족은 HTTP `403`으로 UI에 구분 표시된다. - [ ] 잘못된 Origin의 브라우저 요청은 MCP 서버에서 거부되며, 허용 Origin 및 Origin 없는 네이티브 MCP client 정책이 문서화된다. - [ ] 기존 `kb_vector_mcp` 및 로컬 8500 MCP 회귀 테스트가 통과한다. | 조건 | 현행 판단 | 근거/메모 | |---|---|---| | URL 단일 해석 | 부분 충족 | `_mcp_endpoint()`가 `/mcp` 결합과 URL 검증은 수행하지만 `config/mcp_servers.json`의 `endpoint_url`이 env보다 우선한다. | | 요청별 Authorization header | 충족 | `_jsonrpc_exchange()`와 `_jsonrpc_notification()`이 `Authorization: Bearer `을 header에 설정한다. | | JSON-RPC arguments token 미포함 | 충족 | `build_mcp_tool_arguments()`는 `prompt`, `limit`, `query`, `question` 계열만 구성한다. | | SQLite token 원문 미저장 | 부분 충족 | chat turn에는 선택 사용자/label, question/answer/details를 저장한다. token 원문 필드는 없지만 `details_json`의 evidence payload는 후속 보안 점검 필요. | | 단일 도구 direct routing | 부분 충족 | route가 1개면 single로 처리하지만, 복수 route에서는 LLM router/agent mode를 사용할 수 있다. | | 401/403 구분 | 미충족 | 현재 `401`, `403` 모두 안전한 인증 실패 메시지로 축약한다. | | Origin 검증 | 서버 범위 | UI client는 redirect 차단을 수행한다. Origin 정책은 VPD Backoffice MCP 서버 설정에서 확인해야 한다. | | 벡터 MCP 회귀 | 미검증 | 소스 반입 시 Java 테스트와 Python 문법 검증만 수행했다. 실제 `kb_vector_mcp` smoke test는 후속이다. | ## 4. 컨텍스트 & 제약 - KB VPD MCP endpoint: `https://kb.cloud-handson.com/mcp` - 보호 대상: `ords.query.kb_select_ai_vpd`는 ORDS를 거쳐 VPD 컨텍스트가 적용된 Select AI 조회를 실행한다. - 토큰 주체: VPD 권한은 정적 서비스 계정이 아니라 현재 선택된 `KB_STAKEHOLDERS` 사용자 토큰에 의해 결정된다. - UI의 VPD token preset 파일은 데모 편의 기능일 뿐이다. 운영에서는 OS 소유자 전용 권한(`0600`)으로 관리하고 형상관리·로그·SQLite에서 제외한다. - UI가 현재 사용하는 `2025-11-25` MCP protocol version과 서버의 지원 버전은 handshake에서 협상해야 한다. 지원하지 않는 버전을 무조건 강제하지 않는다. - 현재 서버는 stateless JSON-RPC POST 호출로도 동작한다. 서버가 `Mcp-Session-Id`를 발급하면 client는 이후 요청에만 그 값을 포함한다. ## 5. 아키텍처 개요 I/O는 PoC4 MCP AI Console의 HTTP transport와 VPD Backoffice `/mcp`에 한정한다. URL 결합, 허용 도구 검증, 요청·응답 검증, 안전한 오류 변환은 순수 함수로 분리해 네트워크 없이 테스트한다. ``` VPD 사용자 선택 / 토큰 입력 │ (원문은 요청 메모리에만 존재) ▼ PoC4 MCP AI Console ├─ KB_MCP_BASE_URL + "/mcp" ├─ tool allowlist 검증 └─ Authorization: Bearer │ ▼ HTTPS JSON-RPC / Streamable HTTP VPD Backoffice MCP (/mcp) ├─ Origin·토큰 검증 ├─ tools/list: metadata only └─ tools/call: ords.query.kb_select_ai_vpd │ ▼ ORDS Select AI API → VPD context → Oracle ADB ``` ## 6. 데이터 모델 ### 6.1 KB MCP registry 선언 `config/mcp_servers.json`의 KB 선언은 다음 계약을 따른다. 값은 예시이며, 토큰 값은 넣지 않는다. ```json { "id": "kb_mcp", "enabled": true, "provider": "kb_vpd_streamable_http", "transport": "streamable_http", "base_url_env": "KB_MCP_BASE_URL", "endpoint_path": "/mcp", "auth_delivery": "per_request_vpd_bearer", "timeout_seconds_env": "POC3_MCP_TIMEOUT_SECONDS", "default_tool": "ords.query.kb_select_ai_vpd", "tool_allowlist": ["ords.query.kb_select_ai_vpd"], "router_mode": "direct", "description": "KB VPD Select AI MCP; the current user's VPD bearer is sent only in the Authorization header." } ``` 환경 변수는 아래 두 값만 필요하다. ```dotenv KB_MCP_BASE_URL=https://kb.cloud-handson.com POC3_MCP_TIMEOUT_SECONDS=90 ``` `endpoint_url`, `token_env`, `POC3_MCP_TOKEN`, `BACKOFFICE_MCP_ACCESS_TOKEN`은 KB MCP 선언에 두지 않는다. URL은 registry에 하드코딩하지 않고, VPD 토큰은 사용자별 요청에서만 받는다. ### 6.2 MCP 요청 모든 요청은 다음 헤더를 사용한다. ```http Accept: application/json, text/event-stream Content-Type: application/json MCP-Protocol-Version: Authorization: Bearer ``` `tools/call` body의 `arguments`는 아래와 같이 제한한다. ```json { "name": "ords.query.kb_select_ai_vpd", "arguments": { "prompt": "담당 고객의 보험료 상세를 보여줘", "limit": 50 } } ``` 경계 검증 규칙: - tool name은 정확히 allowlist 값 하나와 일치해야 한다. - `prompt`는 문자열이며 서버와 동일한 최대 길이를 적용한다. - `limit`은 정수 `1..100`으로 clamp한다. - 토큰은 공백·`Bearer ` prefix를 정규화한 뒤 헤더에만 넣는다. - redirect는 허용하지 않는다. 다른 origin으로 Authorization이 전달되어서는 안 된다. ## 7. 함수 명세 (Function Specs) | 함수 | 책임(1줄) | 시그니처(잠정) | 입력 | 출력 | 에러/실패 | 복잡? | |------|-----------|----------------|------|------|-----------|-------| | `resolve_kb_mcp_endpoint` | base URL과 고정 path를 안전하게 결합 | `(server, environ) -> str` | registry, env | HTTPS MCP URL | 누락/비정상 URL | 단순 | | `validate_kb_mcp_server` | KB 선언의 provider·transport·allowlist를 검증 | `(mapping) -> McpServer` | registry row | typed server | 계약 위반 | 단순 | | `mcp_headers` | 요청별 VPD bearer 헤더 생성 | `(token, version, session_id) -> dict` | 사용자 토큰 | 안전한 headers | 토큰 형식 오류 | 단순 | | `discover_kb_tools` | initialize 및 tools/list 후 단일 도구를 검증 | `(server, token) -> McpDiscoveryResult` | endpoint, token | tool descriptor | auth/protocol 오류 | **복잡** | | `call_kb_select_ai` | 고정 도구에 prompt·limit을 전달 | `(server, token, prompt, limit) -> result` | 사용자 요청 | tool result | auth/timeout/JSON-RPC 오류 | **복잡** | | `to_public_mcp_error` | HTTP/JSON-RPC 오류를 안전한 UI 메시지로 변환 | `(exception) -> PublicMcpError` | 내부 오류 | 사용자 메시지 | 원문 노출 금지 | 단순 | ## 8. 흐름 / 알고리즘 1. UI는 `KB_MCP_BASE_URL`을 읽고 `/mcp`만 결합한다. registry의 임의 `endpoint_url`은 KB 서버에 허용하지 않는다. 2. 사용자가 VPD token preset 또는 일회성 Bearer를 선택한다. 토큰은 현재 실행 변수에만 유지한다. 3. client는 `initialize`를 보내고 서버가 반환한 protocol version과 선택 가능한 session ID를 검증한다. 4. `notifications/initialized`를 보낸 뒤 `tools/list`를 실행한다. 5. 응답 목록이 정확히 허용 도구를 포함하는지, 해당 input schema가 `prompt`, `limit` 계약에 맞는지 확인한다. 6. KB 서버는 단일 도구이므로 router model을 호출하지 않고 `ords.query.kb_select_ai_vpd`를 직접 선택한다. 7. `tools/call`은 prompt·clamp된 limit만 body에 넣고 VPD Bearer는 Authorization에만 넣는다. 8. 결과는 화면용 안전 projection만 SQLite에 저장한다. Authorization 헤더와 원문 token은 저장하지 않으며, 진단이 필요하면 단방향 token fingerprint만 별도 보존할 수 있다. ## 9. 엣지케이스 & 에러 처리 | 상황 | client 처리 | 서버 기대 동작 | |------|-------------|----------------| | 토큰 없음 | 호출 전 안내, 네트워크 요청 없음 | 해당 없음 | | 토큰 위조·만료 | `401` → “토큰이 유효하지 않거나 만료됨” | `WWW-Authenticate` 포함 가능 | | 유효하지만 권한 없음 | `403` → “이 사용자에게 조회 권한 없음” | VPD fail-closed 유지 | | allowlist 밖 도구 | 호출 전 차단 | tools/call에서도 차단 | | 429 | 안전하게 재시도하지 않고 잠시 후 재시도 안내 | rate limit 정책 적용 | | timeout | tools/call 자동 재시도 금지 | request ID 기반 감사 추적 | | session ID 미발급 | stateless POST로 진행 | session을 요구하지 않음 | | session ID 발급 | 이후 요청에 `Mcp-Session-Id` 포함 | 세션 소유·만료 검증 | | redirect | 즉시 실패 | Authorization 전달 금지 | | Origin 불일치 | 브라우저 UI에 일반 오류 표시 | `403`으로 거부 | ## 10. 테스트 계획 - registry 단위 테스트 - `KB_MCP_BASE_URL`만으로 endpoint가 `https://kb.cloud-handson.com/mcp`가 되는지 검증 - KB registry에 `endpoint_url`, `token_env`, `custom_python`이 있으면 fail-closed 되는지 검증 - vector MCP 설정은 기존 형식으로 계속 로드되는지 검증 - HTTP transport 단위 테스트 - initialize/tools/list/tools/call 모두 Authorization header가 있고 JSON body에는 token key가 없는지 검증 - `401`, `403`, `429`, timeout, redirect, malformed JSON-RPC 응답을 안전한 메시지로 변환하는지 검증 - session header 반환/재전송 및 stateless fallback을 검증 - 통합 smoke test - 허용된 VPD 사용자 토큰으로 `tools/list`와 `tools/call` 성공 - 잘못된 토큰은 `401`, 타 사용자 권한은 `403` - 설계사와 지점장 토큰으로 동일 질문을 실행해 VPD 행/컬럼 결과가 서로 다른지 확인 - 비밀정보 점검 - chat SQLite, Streamlit log, 예외 메시지에서 토큰 원문 검색 결과 0건 ## 11. 리스크 & 대안 검토 - **선택**: KB MCP 전용 `kb_vpd_streamable_http` 선언을 도입하고, 로컬 8500용 `custom_python`과 분리한다. 외부 HTTPS/VPD Bearer 계약을 코드와 운영 화면에서 명확히 할 수 있다. - **대안 1 — 기존 `custom_python` 재사용**: 동작은 시킬 수 있으나 provider 이름과 endpoint 제약이 실제 KB 서버와 맞지 않아 로컬 MCP와 외부 VPD MCP가 섞인다. - **대안 2 — 고정 MCP access token 사용**: 구현은 간단하지만 모든 사용자가 동일 VPD 주체가 되어 데이터 권한 분리가 무너진다. 채택하지 않는다. - **대안 3 — 즉시 OAuth 2.1 전환**: 표준 상호운용성에는 유리하지만 현재 데모의 VPD token 발급·검증 체계를 대체하므로 별도 인증 서버 설계가 필요하다. - 롤백: 새 registry 선언을 비활성화하고 기존 KB 선언을 복원한다. DB VPD 정책·ORDS endpoint·토큰 데이터는 변경하지 않는다. ## 12. 미해결 질문 (Open Questions) - VPD Backoffice MCP endpoint가 현재 지원할 MCP protocol version을 어떤 값으로 공식 고정할지 결정이 필요하다. - Streamable HTTP의 GET/SSE 및 `Mcp-Session-Id`를 완전 지원할지, stateless POST profile로 운영할지 결정이 필요하다. - 데모 이후 사용자 VPD bearer를 OAuth 2.1 access token으로 전환할지, 현 토큰을 resource-server token으로 계속 운영할지 결정이 필요하다. - chat 대화 이력의 `basis_json`/`details_json`에 민감 데이터 보존 기간과 삭제 정책을 별도로 정해야 한다. ## 13. 2026-07-14 소스 스냅샷 반입 배포 서버의 PoC4 활성 화면 소스를 현재 저장소에 별도 폴더로 반입했다. - 원격 실제 위치: `/home/opc/poc_4/poc4_active_source_20260714.tar.gz` - 사용자 제시 경로 `/home/opc/poc_4/poc4_active_source_20260714/poc4_active_source_20260714.tar.gz`에는 파일이 없었고, 실제 archive는 `/home/opc/poc_4/` 바로 아래에 있었다. - 저장소 위치: `poc4_active_source_20260714/` - 포함 파일: Streamlit UI, MCP router, OCI GenAI client, 모델 profile, token preset sample, 기동/status script, requirements - 보안 확인: 실제 `.env`, 실제 VPD token 원문, 대화 SQLite DB는 포함하지 않았다. `vpd_token_presets.json`에는 placeholder만 있다. - DB 참조 경로: VPD 개발본 배포 서버에서는 `/home/opc/kbmcp/.env`의 접속 정보를 사용하고, wallet directory는 `/home/opc/wallet/kbaipoc`를 사용한다. 두 경로의 파일 내용은 저장소에 포함하지 않는다. - 런타임 전제: Python 3.11 이상. 현재 개발 서버 기본 `python3`는 3.6.8이므로 이 소스의 문법 검증에는 맞지 않는다. - 검증: 배포 서버 원본 경로에서 PoC4 전용 Python `3.11.15`로 `py_compile` 통과. 기존 Java 백오피스 `mvn -q test` 통과. 주의: 이 반입은 소스 스냅샷 보관이다. 아직 본 설계서의 목표 계약인 `kb_vpd_streamable_http`, `KB_MCP_BASE_URL` 단일 endpoint 해석, 단일 도구 direct routing을 구현 완료했다는 의미는 아니다. ## 14. 2026-07-14 역설계 현행화 사용자 요청에 따라 “설계가 먼저 있고 구현을 맞추는 방식”이 아니라, 이미 배포 서버에서 구현·운영 중인 PoC4 MCP AI Console 소스를 기준으로 역설계했다. 현행화한 내용: - `mcp_discovery_ui.py`의 실제 책임 범위를 Streamlit UI, MCP JSON-RPC client, agent loop, evidence collector, answer synthesis, chat store로 분리해 기록했다. - `mcp_servers.json`의 현재 계약이 목표 계약과 다르다는 점을 명시했다. 특히 현행은 `endpoint_url` 우선이며, `provider=custom_python`, `transport=http`이다. - `/home/opc/kbmcp/.env`와 `/home/opc/wallet/kbaipoc`를 배포 서버 DB evidence 조회 기준으로 확정했다. - 포함된 start/status script가 스냅샷 단독 실행용이 아니라 원본 `/home/opc/poc_4` runtime wrapper에 의존한다는 점을 기록했다. - 인수조건을 최종 목표와 현행 충족 상태로 분리했다. 검증: - 배포 서버 원본 소스 기준 Python 3.11.15 `py_compile` 통과 - 백오피스 기존 회귀 `mvn -q test` 통과 - 저장소 문서/스냅샷 커밋 및 `gitea/main` push 완료 ## 15. 2026-07-14 Oracle FGA 감사로그 적용 확인 ### 15.1 확인 목적 PoC4 MCP AI Console과 VPD 백오피스는 사용자별 Bearer token으로 정형 데이터를 조회한다. 이때 Oracle VPD는 행 접근 predicate를 적용하고, ASO/Data Redaction은 민감 컬럼 원문/마스킹을 결정한다. FGA는 그 결과로 실행된 SELECT를 DB 감사 증적으로 남기는 역할이다. 이번 확인은 “FGA가 설계상 있어야 한다”가 아니라, 배포 서버가 바라보는 실제 ADB에 `DBMS_FGA` 정책과 감사 이벤트가 존재하는지 SQLcl로 직접 확인한 결과다. ### 15.2 SQLcl 확인 환경 배포 서버에서 아래 기준으로 접속했다. 비밀번호와 wallet password는 출력하거나 저장소에 기록하지 않는다. | 항목 | 값 | |---|---| | 접속 서버 | `161.33.6.45` | | DB 접속 env | `/home/opc/kbmcp/.env` | | wallet directory | `/home/opc/wallet/kbaipoc` | | SQLcl | `/home/opc/tools/sqlcl/bin/sql` | | 접속 DB 사용자 | `ADMIN` | | 감사 대상 schema | `POC_2` | SQLcl 실행 구조는 다음과 같다. ```bash set -a . /home/opc/kbmcp/.env set +a export TNS_ADMIN="${ORACLE_WALLET_DIR:-/home/opc/wallet/kbaipoc}" /home/opc/tools/sqlcl/bin/sql -s /nolog connect ${ORACLE_DB_USER}/""@${ORACLE_DSN} ``` ### 15.3 적용된 FGA 정책 `DBA_AUDIT_POLICIES`와 `DBA_AUDIT_POLICY_COLUMNS`를 SQLcl로 조회한 결과, `POC_2` schema에는 FGA SELECT 정책 4건이 활성화되어 있다. | 객체 | 정책명 | 감사 컬럼 | 활성 | SELECT 감사 | |---|---|---|---|---| | `KB_CLAIMS` | `KB_FGA_CLAIMS_AMT` | `CLAIM_AMT` | `YES` | `YES` | | `KB_CLAIMS` | `KB_FGA_CLAIMS_PAID_AMT` | `PAID_AMT` | `YES` | `YES` | | `KB_CUSTOMERS` | `KB_FGA_CUSTOMERS_PII` | `CUST_NM`, `RRN_MASKED` | `YES` | `YES` | | `KB_EXTERNAL_HOLDINGS` | `KB_FGA_EXT_HOLDINGS` | `EXT_INSURER`, `EXT_PRODUCT_GRP`, `EXT_PRODUCT_TYPE` | `YES` | `YES` | 요약 쿼리 결과: ```text POLICY_COUNT = 4 ENABLED_COUNT = 4 SELECT_POLICY_COUNT = 4 ``` 현재 DB의 정책명 계열은 모두 `KB_FGA_*`다. 저장소의 범용 적용 스크립트 `sql/adb/42_agent_ords_fga_execution_audit.sql`은 `CB_VPD_EXEC_AUDIT_` 형태의 정책을 만들도록 작성되어 있으나, 현행 ADB에는 이 계열이 아니라 `KB_FGA_*` 정책이 적용되어 있다. 따라서 운영 확인 시에는 “스크립트 파일명/예상명”보다 `DBA_AUDIT_POLICIES`의 실제 정책명을 기준으로 봐야 한다. ### 15.4 감사 이벤트 저장 위치 ADB 현행 환경에서는 FGA 이벤트가 `UNIFIED_AUDIT_TRAIL`에 기록된다. SQLcl 확인 결과 `UNIFIED_AUDIT_TRAIL`에는 FGA 조회에 필요한 아래 컬럼이 있다. | 컬럼 | 용도 | |---|---| | `EVENT_TIMESTAMP`, `EVENT_TIMESTAMP_UTC` | 감사 발생 시각 | | `DBUSERNAME` | SQL을 실행한 DB 사용자 | | `CLIENT_IDENTIFIER` | ORDS/MCP 요청 식별자 | | `OBJECT_SCHEMA`, `OBJECT_NAME` | 감사 대상 객체 | | `ACTION_NAME` | 실행 작업. 현재는 `SELECT` | | `FGA_POLICY_NAME` | 트리거된 FGA 정책명 | | `SQL_TEXT` | DB가 감사한 실제 SQL | | `RLS_INFO` | Oracle이 기록한 VPD 정책명과 predicate | | `RETURN_CODE` | 실행 결과 코드. `0`이면 성공 | 최근 7일 FGA 이벤트 요약: ```text UNIFIED_AUDIT_TRAIL event_count_7d = 525 oldest_event = 2026-07-09 06:15:04 newest_event = 2026-07-14 05:09:14 ``` 반면 `DBA_FGA_AUDIT_TRAIL` 기준 최근 7일 이벤트는 0건이었다. ```text DBA_FGA_AUDIT_TRAIL event_count_7d = 0 ``` 해석은 다음과 같다. - FGA가 미적용이라는 뜻이 아니다. - 현재 ADB에서는 FGA 감사 행이 Unified Audit Trail에 기록되고 있다. - 앱 구현도 이 전제를 반영해 `UNIFIED_AUDIT_TRAIL`을 먼저 조회하고, 실패할 때 `DBA_FGA_AUDIT_TRAIL`로 fallback한다. ### 15.5 최근 감사 이벤트 예시 최근 이벤트는 모두 `CB_ORDS` DB 사용자로 기록되었고, `CLIENT_IDENTIFIER`에는 요청별 UUID가 들어가 있었다. 이 값으로 특정 ORDS/MCP 요청과 DB 감사 행을 연결한다. 확인된 최근 이벤트 예: | 발생시각(KST) | DB 사용자 | 객체 | 정책 | 작업 | 결과 | |---|---|---|---|---|---| | 2026-07-14 14:09:14 | `CB_ORDS` | `KB_EXTERNAL_HOLDINGS` | `KB_FGA_EXT_HOLDINGS` | `SELECT` | `0` | | 2026-07-14 14:06:31 | `CB_ORDS` | `KB_EXTERNAL_HOLDINGS` | `KB_FGA_EXT_HOLDINGS` | `SELECT` | `0` | | 2026-07-14 14:06:31 | `CB_ORDS` | `KB_CLAIMS` | `KB_FGA_CLAIMS_AMT` | `SELECT` | `0` | | 2026-07-14 14:06:31 | `CB_ORDS` | `KB_CLAIMS` | `KB_FGA_CLAIMS_PAID_AMT` | `SELECT` | `0` | `RLS_INFO`도 같이 기록된다. 예를 들어 `KB_CLAIMS` 이벤트에는 `KB_KB_CLAIMS_ROW_POLICY`와 해당 VPD predicate가 포함되어 있었다. 즉 FGA는 “어떤 SQL이 실행됐는가”뿐 아니라 “그 SQL에 어떤 VPD 정책이 붙었는가”를 사후 증적으로 확인하는 데 사용할 수 있다. ### 15.6 소스 구현과 화면 연결 PoC4 스냅샷의 `apps/poc4/mcp_discovery_ui.py`는 감사로그 탭에서 다음 두 쿼리를 사용한다. | 함수 | 조회 대상 | 역할 | |---|---|---| | `_load_fga_inventory()` | `DBA_AUDIT_POLICIES`, `DBA_AUDIT_POLICY_COLUMNS`, `POC_2.KB_SECURITY_POLICY_CATALOG` | 현재 활성 FGA 정책과 관리 카탈로그를 표시 | | `_load_fga_audit_events()` | `UNIFIED_AUDIT_TRAIL` | 최근 FGA 이벤트, SQL 원문, 사용자, 정책명, 결과 코드를 표시 | VPD 백오피스의 검증 화면은 `OrdsProbeService`에서 다음 순서로 특정 요청의 FGA 증적을 찾는다. 1. `UNIFIED_AUDIT_TRAIL` 2. `DBA_FGA_AUDIT_TRAIL` 조회 조건은 객체 owner/name, `ACTION_NAME = 'SELECT'`, `CLIENT_IDENTIFIER = `, `FGA_POLICY_NAME IS NOT NULL`이다. 따라서 운영자가 봐야 할 기준은 다음이다. - “정책이 적용됐는가?” → `DBA_AUDIT_POLICIES.ENABLED = YES` - “민감 컬럼 접근이 실제 발생했는가?” → `UNIFIED_AUDIT_TRAIL.FGA_POLICY_NAME IS NOT NULL` - “어떤 VPD predicate가 붙었는가?” → `UNIFIED_AUDIT_TRAIL.RLS_INFO` - “어떤 요청과 연결되는가?” → `CLIENT_IDENTIFIER` ### 15.7 확인용 SQL 운영 확인 시 사용할 수 있는 최소 SQL은 다음과 같다. ```sql SELECT policy.object_schema, policy.object_name, policy.policy_name, policy.enabled, policy.sel, LISTAGG(policy_columns.policy_column, ',') WITHIN GROUP ( ORDER BY policy_columns.policy_column ) AS policy_columns FROM dba_audit_policies policy LEFT JOIN dba_audit_policy_columns policy_columns ON policy_columns.object_schema = policy.object_schema AND policy_columns.object_name = policy.object_name AND policy_columns.policy_name = policy.policy_name WHERE policy.object_schema = 'POC_2' GROUP BY policy.object_schema, policy.object_name, policy.policy_name, policy.enabled, policy.sel ORDER BY policy.object_name, policy.policy_name; ``` ```sql SELECT * FROM ( SELECT TO_CHAR( event_timestamp AT TIME ZONE 'Asia/Seoul', 'YYYY-MM-DD HH24:MI:SS' ) AS event_time, dbusername, client_identifier, object_name, fga_policy_name, action_name, return_code, DBMS_LOB.SUBSTR(sql_text, 500, 1) AS sql_text_sample, DBMS_LOB.SUBSTR(rls_info, 500, 1) AS rls_info_sample FROM unified_audit_trail WHERE object_schema = 'POC_2' AND fga_policy_name IS NOT NULL ORDER BY event_timestamp DESC ) WHERE ROWNUM <= 20; ``` ### 15.8 후속 정리 필요사항 현재 FGA 정책 자체는 정상 적용되어 있고 감사 이벤트도 쌓이고 있다. 다만 관리 카탈로그 `POC_2.KB_SECURITY_POLICY_CATALOG`에는 `KB_CUSTOMERS`, `KB_EXTERNAL_HOLDINGS` 중심의 5개 row만 확인되며, 실제 활성 정책에 있는 `KB_CLAIMS.CLAIM_AMT`, `KB_CLAIMS.PAID_AMT` 정책 메타데이터는 카탈로그 조회 결과에 없었다. 따라서 화면이 `DBA_AUDIT_POLICIES`를 직접 조회하는 현재 구조에서는 정책이 보이지만, 카탈로그를 운영 기준으로 삼으려면 `KB_CLAIMS` FGA 메타데이터도 `KB_SECURITY_POLICY_CATALOG`에 맞춰 등록하는 정리가 필요하다.