#!/usr/bin/env node import fs from "node:fs"; import path from "node:path"; import zlib from "node:zlib"; const DEFAULT_OUTPUT_DIR = "docs/architecture"; const libraryInput = process.argv[2] || process.env.OCI_DRAWIO_LIBRARY; if (!libraryInput) { throw new Error( "OCI Draw.io library path is required as the first argument or OCI_DRAWIO_LIBRARY", ); } const libraryPath = path.resolve(libraryInput); const outputDir = path.resolve(process.argv[3] || DEFAULT_OUTPUT_DIR); const COLORS = { ink: "#312D2A", muted: "#5B5652", oracleRed: "#C74634", oracleOrange: "#AE562C", regionFill: "#F5F4F2", regionStroke: "#9E9892", panel: "#FFFFFF", panelSoft: "#FCFBFA", thirdParty: "#6B7280", success: "#2F6B4F", blue: "#2F6F8F", }; const REQUIRED_ICONS = [ "Identity and Security - User", "Compute - Virtual Machine VM", "Database - Autonomous DB", "Analytics and AI - Artificial Intelligence", "Storage - Object Storage", "Identity and Security - Vault", ]; function escapeXml(value) { return String(value) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """); } function readLibrary(file) { const raw = fs.readFileSync(file, "utf8").trim(); const prefix = ""; const suffix = ""; if (!raw.startsWith(prefix) || !raw.endsWith(suffix)) { throw new Error(`Invalid Draw.io library: ${file}`); } const entries = JSON.parse(raw.slice(prefix.length, -suffix.length)); const byTitle = new Map(entries.map((entry) => [entry.title, entry])); const missing = REQUIRED_ICONS.filter((title) => !byTitle.has(title)); if (missing.length) { throw new Error(`Required OCI library shapes not found: ${missing.join(", ")}`); } return byTitle; } function decodeLibraryEntry(entry) { const encoded = Buffer.from(entry.xml, "base64"); return decodeURIComponent(zlib.inflateRawSync(encoded).toString("utf8")); } function style(parts) { return Object.entries(parts) .filter(([, value]) => value !== undefined && value !== null) .map(([key, value]) => `${key}=${value}`) .join(";") + ";"; } function createPage(name, id, width, height) { let sequence = 1; const cells = [ `` + `` + ``, ]; const nextId = (prefix = "c") => `${id}_${prefix}_${sequence++}`; function vertex({ x, y, w, h, value = "", cellStyle, parent = "1", cellId = nextId("v"), connectable, }) { const connectableAttr = connectable === false ? ' connectable="0"' : ""; cells.push( `` + `` + ``, ); return cellId; } function edge({ source, target, value = "", cellStyle, points = [], cellId = nextId("e"), }) { const pointXml = points.length ? `${points.map(([x, y]) => ``).join("")}` : ""; cells.push( `` + `${pointXml}` + ``, ); return cellId; } function officialIcon(library, title, x, y, parent = "1") { const entry = library.get(title); const decoded = decodeLibraryEntry(entry); const sourceCells = [ ...decoded.matchAll(/]*\/>|]*>[\s\S]*?<\/mxCell>/g), ].map((match) => match[0]); const parsed = sourceCells .map((xml) => ({ xml, id: xml.match(/\bid="([^"]+)"/)?.[1], parent: xml.match(/\bparent="([^"]+)"/)?.[1], value: xml.match(/\bvalue="([^"]*)"/)?.[1] ?? "", })) .filter((cell) => cell.id && !["0", "1"].includes(cell.id)); const included = parsed.filter((cell) => !cell.value.trim()); const wrapperId = nextId("oci"); cells.push( `` + `` + ``, ); const idMap = new Map(included.map((cell) => [cell.id, nextId("ociPart")])); for (const cell of included) { let xml = cell.xml; xml = xml.replace(/\bid="([^"]+)"/, `id="${idMap.get(cell.id)}"`); xml = xml.replace(/\bparent="([^"]+)"/, (_match, oldParent) => { if (oldParent === "1") { return `parent="${wrapperId}"`; } return `parent="${idMap.get(oldParent) || wrapperId}"`; }); xml = xml.replace(/\b(source|target)="([^"]+)"/g, (_match, attr, oldId) => `${attr}="${idMap.get(oldId) || oldId}"`); cells.push(xml); } return wrapperId; } function result() { const model = [ ``, "", '', '', ...cells, "", "", ].join(""); return `${model}`; } return { vertex, edge, officialIcon, result }; } const labelStyle = (options = {}) => style({ rounded: 0, whiteSpace: "wrap", html: 1, strokeColor: "none", fillColor: "none", align: options.align || "left", verticalAlign: options.verticalAlign || "middle", fontFamily: "Oracle Sans", fontColor: options.fontColor || COLORS.ink, fontSize: options.fontSize || 12, fontStyle: options.bold ? 1 : 0, spacingLeft: options.spacingLeft || 0, spacingRight: options.spacingRight || 0, }); const panelStyle = (options = {}) => style({ rounded: 1, arcSize: options.arcSize || 8, whiteSpace: "wrap", html: 1, fillColor: options.fillColor || COLORS.panel, strokeColor: options.strokeColor || COLORS.regionStroke, strokeWidth: options.strokeWidth || 1, dashed: options.dashed ? 1 : 0, fontFamily: "Oracle Sans", fontColor: options.fontColor || COLORS.ink, fontSize: options.fontSize || 12, align: options.align || "center", verticalAlign: options.verticalAlign || "middle", spacing: options.spacing || 6, spacingLeft: options.spacingLeft, spacingRight: options.spacingRight, }); const edgeStyle = (options = {}) => style({ edgeStyle: "orthogonalEdgeStyle", rounded: 1, orthogonalLoop: 1, jettySize: "auto", html: 1, strokeColor: options.strokeColor || COLORS.ink, strokeWidth: options.strokeWidth || 2, dashed: options.dashed ? 1 : 0, endArrow: options.endArrow || "block", endFill: options.endFill === false ? 0 : 1, fontFamily: "Oracle Sans", fontColor: options.fontColor || COLORS.ink, fontSize: options.fontSize || 11, labelBackgroundColor: "#FFFFFF", exitX: options.exitX, exitY: options.exitY, entryX: options.entryX, entryY: options.entryY, }); function title(page, heading, subtitle, width) { page.vertex({ x: 36, y: 22, w: width - 72, h: 38, value: heading, cellStyle: labelStyle({ fontSize: 25, bold: true }), }); page.vertex({ x: 38, y: 60, w: width - 76, h: 28, value: subtitle, cellStyle: labelStyle({ fontSize: 12, fontColor: COLORS.muted }), }); page.vertex({ x: 36, y: 92, w: width - 72, h: 3, cellStyle: panelStyle({ fillColor: COLORS.oracleRed, strokeColor: COLORS.oracleRed, arcSize: 0, }), }); } function boundary(page, x, y, w, h, heading, options = {}) { const box = page.vertex({ x, y, w, h, value: "", cellStyle: panelStyle({ fillColor: options.fillColor || COLORS.regionFill, strokeColor: options.strokeColor || COLORS.regionStroke, strokeWidth: options.strokeWidth || 1, dashed: options.dashed, arcSize: options.arcSize || 7, }), }); page.vertex({ x: x + 14, y: y + 8, w: w - 28, h: 26, value: `${heading}`, cellStyle: labelStyle({ fontSize: options.fontSize || 13, fontColor: options.headingColor || COLORS.ink, }), }); return box; } function addOverviewPage(library) { const page = createPage("01 · 전체 구성", "hmm-overview", 1800, 1080); title( page, "HMM AI 데이터 접근 아키텍처", "사용자 Bearer MCP · Select AI · VPD · HR 정책 벡터 검색 · AWS RDS PostgreSQL Federation", 1800, ); boundary(page, 30, 120, 190, 790, "HMM 사용자", { fillColor: COLORS.panelSoft, }); page.officialIcon(library, "Identity and Security - User", 80, 165); const users = page.vertex({ x: 54, y: 270, w: 142, h: 150, value: "팀장 · E1001
팀원 · E1002~E1007
HR 관리자

같은 질문이라도 역할과 보고 관계에 따라 다른 행을 조회", cellStyle: panelStyle({ fillColor: "#FFFFFF", strokeColor: "#D4CFCA" }), }); const externalAgent = page.vertex({ x: 54, y: 465, w: 142, h: 115, value: "외부 Agent
AI Database Private
Agent Factory

MCP client", cellStyle: panelStyle({ fillColor: "#FFFFFF", strokeColor: "#D4CFCA" }), }); page.vertex({ x: 54, y: 625, w: 142, h: 210, value: "공개 HTTPS

hmm.cloud-handson.com

hmm-backoffice.cloud-handson.com/mcp

사용자 Bearer Token
TLS · HttpOnly portal cookie", cellStyle: panelStyle({ fillColor: "#FFFFFF", strokeColor: COLORS.oracleOrange }), }); boundary(page, 250, 120, 1260, 900, "Oracle Cloud Infrastructure", { fillColor: "#FBFAF9", }); boundary(page, 280, 165, 930, 815, "OCI Region · ap-seoul-1", { fillColor: COLORS.regionFill, }); boundary(page, 315, 210, 480, 320, "VCN · Public application subnet", { fillColor: "#FFFFFF", strokeColor: COLORS.oracleOrange, dashed: true, headingColor: COLORS.oracleOrange, }); page.officialIcon(library, "Compute - Virtual Machine VM", 340, 265); const compute = page.vertex({ x: 445, y: 260, w: 320, h: 235, value: "", cellStyle: panelStyle({ fillColor: "#FCFBFA", strokeColor: "#D4CFCA" }), }); page.vertex({ x: 462, y: 274, w: 286, h: 34, value: "Compute VM · 132.226.232.69", cellStyle: labelStyle({ fontSize: 13, bold: true }), }); const nginx = page.vertex({ x: 470, y: 320, w: 270, h: 42, value: "Nginx · TLS / reverse proxy", cellStyle: panelStyle({ fillColor: "#FFFFFF", strokeColor: "#D4CFCA" }), }); const consoleApp = page.vertex({ x: 470, y: 376, w: 270, h: 46, value: "AI Web Agent Console
Streamlit · 질문/근거/감사 UI", cellStyle: panelStyle({ fillColor: "#FFFFFF", strokeColor: COLORS.blue }), }); const backoffice = page.vertex({ x: 470, y: 436, w: 270, h: 46, value: "VPD Backoffice
Spring Boot · `/mcp` · 권한 관리", cellStyle: panelStyle({ fillColor: "#FFFFFF", strokeColor: COLORS.oracleRed }), }); page.officialIcon(library, "Identity and Security - Vault", 330, 635); const vault = page.vertex({ x: 420, y: 642, w: 160, h: 76, value: "Vault / Secrets
DB·OCI 자격증명
Git 외부 관리", cellStyle: panelStyle({ fillColor: "#FFFFFF", strokeColor: "#D4CFCA" }), }); page.officialIcon(library, "Storage - Object Storage", 330, 800); const objectStorage = page.vertex({ x: 445, y: 804, w: 180, h: 82, value: "Object Storage
HR 규정 PDF 원본
파일명·생성일 메타데이터", cellStyle: panelStyle({ fillColor: "#FFFFFF", strokeColor: "#D4CFCA" }), }); page.officialIcon(library, "Database - Autonomous DB", 805, 225); const adb = page.vertex({ x: 905, y: 220, w: 270, h: 690, value: "", cellStyle: panelStyle({ fillColor: "#FFFFFF", strokeColor: COLORS.oracleRed, strokeWidth: 2 }), }); page.vertex({ x: 920, y: 238, w: 238, h: 40, value: "Autonomous Database · HMMAIPOC", cellStyle: labelStyle({ fontSize: 14, bold: true }), }); const startupGate = page.vertex({ x: 925, y: 294, w: 230, h: 72, value: "MCP Tool 계약
BACKOFFICE_MCP_TOOLS
시작 시 USER_AI_AGENT_TOOLS.ENABLED 검증", cellStyle: panelStyle({ fillColor: "#FFF7F2", strokeColor: COLORS.oracleOrange }), }); const agentTools = page.vertex({ x: 925, y: 382, w: 230, h: 78, value: "ADB Agent Tools
resolve_hr_term
search_hr_policy
DBMS_CLOUD_AI_AGENT.RUN_TOOL", cellStyle: panelStyle({ fillColor: "#FCFBFA", strokeColor: "#D4CFCA" }), }); const selectAi = page.vertex({ x: 925, y: 475, w: 230, h: 80, value: "search_hr_data · SELECT_AI
ADMIN: SHOWSQL 생성/검증
CB_ORDS: 읽기 전용 SQL 실행", cellStyle: panelStyle({ fillColor: "#F4F8FA", strokeColor: COLORS.blue }), }); const vpd = page.vertex({ x: 925, y: 570, w: 230, h: 92, value: "Bearer → Secure Context → VPD
HMM_ACCESS_CTX
HMM_LEAVE_SCOPE_POLICY
SELF · MANAGED_TEAM · ALL", cellStyle: panelStyle({ fillColor: "#FFF7F2", strokeColor: COLORS.oracleRed }), }); const hrData = page.vertex({ x: 925, y: 678, w: 230, h: 72, value: "HMM HR 정형 데이터
직원·팀·휴가·근태
역할·그룹·권한·Token hash", cellStyle: panelStyle({ fillColor: "#FCFBFA", strokeColor: "#D4CFCA" }), }); const vectorData = page.vertex({ x: 925, y: 765, w: 230, h: 62, value: "정책 지식
Abstract · Tag · Chunk · Embed 4 Vector", cellStyle: panelStyle({ fillColor: "#FCFBFA", strokeColor: "#D4CFCA" }), }); const federation = page.vertex({ x: 925, y: 842, w: 230, h: 52, value: "Federation View
HMM_RDS_*_V · 선사 배정 View", cellStyle: panelStyle({ fillColor: "#FCFBFA", strokeColor: "#D4CFCA" }), }); boundary(page, 1235, 165, 240, 300, "OCI Region · us-chicago-1", { fillColor: COLORS.regionFill, }); page.officialIcon(library, "Analytics and AI - Artificial Intelligence", 1260, 220); const genai = page.vertex({ x: 1260, y: 320, w: 190, h: 110, value: "OCI Generative AI

GPT-5.4 Mini
질문 계획 · SQL 생성 · 답변

Cohere Embed 4
용어·문서 Vector", cellStyle: panelStyle({ fillColor: "#FFFFFF", strokeColor: COLORS.oracleOrange }), }); boundary(page, 1535, 120, 235, 900, "3rd Party Cloud · AWS", { fillColor: "#FFFFFF", strokeColor: COLORS.thirdParty, dashed: true, headingColor: COLORS.thirdParty, }); const rds = page.vertex({ x: 1570, y: 530, w: 165, h: 180, value: "AWS RDS PostgreSQL

hmm_demo.carriers
carrier_monthly_performance

가상 선사 8개
월간 실적 144행
", cellStyle: style({ shape: "cylinder3", boundedLbl: 1, backgroundOutline: 1, size: 15, whiteSpace: "wrap", html: 1, fillColor: "#F9FAFB", strokeColor: COLORS.thirdParty, fontFamily: "Oracle Sans", fontColor: COLORS.ink, fontSize: 12, spacingTop: 12, }), }); page.edge({ source: users, target: nginx, value: "HTTPS · 질문/관리", cellStyle: edgeStyle({ strokeColor: COLORS.blue, exitX: 1, exitY: 0.35, entryX: 0, entryY: 0.5 }), }); page.edge({ source: externalAgent, target: backoffice, value: "MCP initialize · tools/list · tools/call", cellStyle: edgeStyle({ strokeColor: COLORS.oracleRed, exitX: 1, exitY: 0.5, entryX: 0, entryY: 0.5 }), }); page.edge({ source: nginx, target: consoleApp, value: "", cellStyle: edgeStyle({ strokeColor: COLORS.blue, exitX: 0.5, exitY: 1, entryX: 0.5, entryY: 0 }), }); page.edge({ source: consoleApp, target: backoffice, value: "MCP", cellStyle: edgeStyle({ strokeColor: COLORS.oracleRed, exitX: 1, exitY: 0.5, entryX: 1, entryY: 0.5 }), points: [[780, 399], [780, 459]], }); page.edge({ source: backoffice, target: startupGate, value: "JDBC · Tool 호출", cellStyle: edgeStyle({ strokeColor: COLORS.oracleRed, exitX: 1, exitY: 0.5, entryX: 0, entryY: 0.5 }), points: [[805, 459], [885, 459], [885, 330]], }); page.edge({ source: startupGate, target: agentTools, value: "ENABLED", cellStyle: edgeStyle({ strokeColor: COLORS.success, exitX: 0.5, exitY: 1, entryX: 0.5, entryY: 0 }), }); page.edge({ source: startupGate, target: selectAi, value: "Java Tool", cellStyle: edgeStyle({ strokeColor: COLORS.blue, exitX: 1, exitY: 0.5, entryX: 1, entryY: 0.5 }), points: [[1170, 330], [1170, 515]], }); page.edge({ source: selectAi, target: vpd, value: "동일 CB_ORDS session", cellStyle: edgeStyle({ strokeColor: COLORS.oracleRed, exitX: 0.5, exitY: 1, entryX: 0.5, entryY: 0 }), }); page.edge({ source: vpd, target: hrData, value: "predicate 자동 적용", cellStyle: edgeStyle({ strokeColor: COLORS.oracleRed, exitX: 0.5, exitY: 1, entryX: 0.5, entryY: 0 }), }); page.edge({ source: objectStorage, target: vectorData, value: "PDF ingest · 청킹", cellStyle: edgeStyle({ dashed: true, strokeColor: COLORS.oracleOrange, exitX: 1, exitY: 0.5, entryX: 0, entryY: 0.5 }), }); page.edge({ source: vault, target: compute, value: "runtime secret", cellStyle: edgeStyle({ dashed: true, strokeColor: COLORS.regionStroke, exitX: 0.5, exitY: 0, entryX: 0, entryY: 1 }), }); page.edge({ source: adb, target: genai, value: "모델 호출", cellStyle: edgeStyle({ dashed: true, strokeColor: COLORS.oracleOrange, exitX: 1, exitY: 0.28, entryX: 0, entryY: 0.55 }), }); page.edge({ source: federation, target: rds, value: "HMM_RDS_PG_LINK · TLS 5432", cellStyle: edgeStyle({ strokeColor: COLORS.thirdParty, exitX: 1, exitY: 0.5, entryX: 0, entryY: 0.5 }), }); page.vertex({ x: 1545, y: 830, w: 215, h: 105, value: "읽기 책임 분리
RDS: 선사 실적 원장
ADB: 직원·배정·권한·VPD
MCP: 허용된 View만 자연어 조회", cellStyle: panelStyle({ fillColor: "#F9FAFB", strokeColor: COLORS.thirdParty }), }); page.vertex({ x: 280, y: 985, w: 1195, h: 24, value: "※ hmm-mcp.cloud-handson.com은 공용 호환 게이트웨이입니다. 사용자별 VPD MCP의 기준 주소는 hmm-backoffice.cloud-handson.com/mcp입니다.", cellStyle: labelStyle({ fontSize: 11, fontColor: COLORS.muted }), }); return page.result(); } function numberedBox(page, number, x, y, w, h, heading, body, options = {}) { const box = page.vertex({ x, y, w, h, value: `${heading}
${body}`, cellStyle: panelStyle({ fillColor: options.fillColor || "#FFFFFF", strokeColor: options.strokeColor || "#D4CFCA", align: "left", verticalAlign: "middle", spacing: 10, spacingLeft: 22, spacingRight: 8, fontSize: options.fontSize || 11, }), }); page.vertex({ x: x - 12, y: y + 10, w: 30, h: 30, value: `${number}`, cellStyle: style({ ellipse: 1, shape: "ellipse", whiteSpace: "wrap", html: 1, fillColor: options.badgeColor || COLORS.oracleRed, strokeColor: options.badgeColor || COLORS.oracleRed, fontFamily: "Oracle Sans", fontSize: 13, align: "center", verticalAlign: "middle", }), }); return box; } function addSecurityFlowPage(library) { const page = createPage("02 · MCP 요청과 VPD", "hmm-security-flow", 1800, 1050); title( page, "MCP Tool Discovery와 사용자별 VPD 실행 흐름", "광고 계약 검증은 시작 시 1회 · 사용자 인증과 Context 설정·SQL 실행·정리는 요청마다 같은 세션에서 수행", 1800, ); boundary(page, 35, 120, 1730, 185, "서버 시작 · Tool 광고 전 Fail-Closed Gate", { fillColor: "#FFF7F2", strokeColor: COLORS.oracleOrange, headingColor: COLORS.oracleOrange, }); const config = numberedBox(page, "A", 95, 185, 300, 78, "BACKOFFICE_MCP_TOOLS", "공개 이름 · 설명 · 입력 schema
AGENT_TOOL targetName", { strokeColor: COLORS.oracleOrange, badgeColor: COLORS.oracleOrange, }); const validator = numberedBox(page, "B", 510, 185, 330, 78, "McpAgentToolStartupValidator", "AGENT_TOOL targetName 중복 제거
SELECT_AI는 DB Tool 검증에서 제외", { strokeColor: COLORS.oracleOrange, badgeColor: COLORS.oracleOrange, }); const metadata = numberedBox(page, "C", 955, 185, 330, 78, "USER_AI_AGENT_TOOLS", "Tool 존재 + STATUS=ENABLED 확인", { strokeColor: COLORS.oracleOrange, badgeColor: COLORS.oracleOrange, }); const ready = numberedBox(page, "D", 1400, 185, 285, 78, "Endpoint Ready", "모두 정상일 때만 tools/list 광고
누락·비활성·조회 오류는 기동 실패", { strokeColor: COLORS.success, badgeColor: COLORS.success, }); page.edge({ source: config, target: validator, value: "구성 로드", cellStyle: edgeStyle({ strokeColor: COLORS.oracleOrange }) }); page.edge({ source: validator, target: metadata, value: "시작 시 1회 조회", cellStyle: edgeStyle({ strokeColor: COLORS.oracleOrange }) }); page.edge({ source: metadata, target: ready, value: "모두 ENABLED", cellStyle: edgeStyle({ strokeColor: COLORS.success }) }); boundary(page, 35, 330, 225, 665, "MCP Client", { fillColor: "#F9FAFB" }); page.officialIcon(library, "Identity and Security - User", 95, 385); const client = numberedBox(page, 1, 70, 500, 160, 100, "Agent 요청", "initialize
tools/list
tools/call", { strokeColor: COLORS.blue, badgeColor: COLORS.blue, }); const response = numberedBox(page, 10, 70, 800, 160, 115, "필터링 결과", "허용 행 + 근거만 반환
Token·내부 SQL은 미노출", { strokeColor: COLORS.success, badgeColor: COLORS.success, }); boundary(page, 285, 330, 650, 665, "VPD Backoffice · Spring Boot", { fillColor: "#FCFBFA", }); const auth = numberedBox(page, 2, 330, 400, 250, 110, "Bearer 인증", "Token SHA-256 hash
만료·회수·재직 상태 검증
실패: HTTP 401", { strokeColor: COLORS.oracleRed, }); const router = numberedBox(page, 3, 650, 400, 235, 110, "MCP Tool Router", "catalog에서 이름·인자 검증
AGENT_TOOL / SELECT_AI 분기", { strokeColor: COLORS.oracleRed, }); const agentTool = numberedBox(page, 4, 350, 590, 250, 130, "ADB AGENT_TOOL", "resolve_hr_term · search_hr_policy
동일 기본 JDBC session
SET_USER_BY_BEARER
→ RUN_TOOL → CLEAR_USER", { strokeColor: COLORS.oracleOrange, badgeColor: COLORS.oracleOrange, fontSize: 10, }); const showSql = numberedBox(page, 5, 650, 590, 235, 125, "search_hr_data", "ADMIN 생성 세션
DBMS_CLOUD_AI.GENERATE
action=showsql
SELECT/WITH + 승인 객체 검증", { strokeColor: COLORS.blue, badgeColor: COLORS.blue, }); page.edge({ source: client, target: auth, value: "Authorization: Bearer …", cellStyle: edgeStyle({ strokeColor: COLORS.oracleRed }) }); page.edge({ source: auth, target: router, value: "인증된 employee", cellStyle: edgeStyle({ strokeColor: COLORS.oracleRed }) }); page.edge({ source: router, target: agentTool, value: "AGENT_TOOL", cellStyle: edgeStyle({ strokeColor: COLORS.oracleOrange, exitX: 0.25, exitY: 1, entryX: 0.5, entryY: 0 }) }); page.edge({ source: router, target: showSql, value: "SELECT_AI", cellStyle: edgeStyle({ strokeColor: COLORS.blue, exitX: 0.75, exitY: 1, entryX: 0.5, entryY: 0 }) }); boundary(page, 960, 330, 805, 665, "Autonomous Database · 보안 실행 경계", { fillColor: COLORS.regionFill, strokeColor: COLORS.oracleRed, }); page.officialIcon(library, "Database - Autonomous DB", 995, 370); const setContext = numberedBox(page, 6, 1100, 390, 285, 110, "CB_ORDS 동일 JDBC 세션", "CB_ORDS_HANDLER_PKG
SET_VPD_CONTEXT
Runtime principal·context 일치 검증", { strokeColor: COLORS.oracleRed, }); const context = numberedBox(page, 7, 1445, 390, 270, 110, "Secure Application Context", "HMM_ACCESS_CTX_PKG
SET_USER_BY_BEARER
EMPLOYEE_ID · CODE · TEAM_ID", { strokeColor: COLORS.oracleRed, }); const execute = numberedBox(page, 8, 1100, 585, 285, 125, "읽기 전용 SQL 실행", "CB_ORDS · SET TRANSACTION READ ONLY
EXEMPT ACCESS POLICY 없음
승인된 HR Table/View만 조회", { strokeColor: COLORS.blue, badgeColor: COLORS.blue, }); const policy = numberedBox(page, "8a", 1445, 585, 270, 125, "HMM_LEAVE_SCOPE_POLICY", "SYS_CONTEXT 기반 predicate 자동 적용
VIEWER → SELF
MANAGER → MANAGED_TEAM
ADMIN role → ALL", { strokeColor: COLORS.oracleRed, }); const cleanup = numberedBox(page, 9, 1100, 805, 615, 95, "항상 정리", "결과 읽기 → ROLLBACK → CB_ORDS_HANDLER_PKG · CLEAR_VPD_CONTEXT
성공·실패와 무관하게 session context와 client identifier 제거", { strokeColor: COLORS.success, badgeColor: COLORS.success, }); page.edge({ source: showSql, target: setContext, value: "검증된 SQL + 원문 Bearer", cellStyle: edgeStyle({ strokeColor: COLORS.oracleRed }) }); page.edge({ source: agentTool, target: response, value: "Agent Tool 결과 · CLEAR_USER", cellStyle: edgeStyle({ dashed: true, strokeColor: COLORS.success, exitX: 1, exitY: 0.5, entryX: 1, entryY: 0.5 }), points: [[635, 655], [635, 760], [280, 760], [280, 855]], }); page.edge({ source: setContext, target: context, value: "SET_USER_BY_BEARER", cellStyle: edgeStyle({ strokeColor: COLORS.oracleRed }) }); page.edge({ source: setContext, target: execute, value: "context 확인 후 실행", cellStyle: edgeStyle({ strokeColor: COLORS.blue, exitX: 0.5, exitY: 1, entryX: 0.5, entryY: 0 }) }); page.edge({ source: context, target: policy, value: "SYS_CONTEXT", cellStyle: edgeStyle({ strokeColor: COLORS.oracleRed, exitX: 0.5, exitY: 1, entryX: 0.5, entryY: 0 }) }); page.edge({ source: execute, target: policy, value: "SELECT → predicate 주입", cellStyle: edgeStyle({ strokeColor: COLORS.oracleRed }) }); page.edge({ source: policy, target: cleanup, value: "허용 행", cellStyle: edgeStyle({ strokeColor: COLORS.success, exitX: 0.5, exitY: 1, entryX: 0.8, entryY: 0 }) }); page.edge({ source: execute, target: cleanup, value: "result set", cellStyle: edgeStyle({ strokeColor: COLORS.success, exitX: 0.5, exitY: 1, entryX: 0.2, entryY: 0 }) }); page.edge({ source: cleanup, target: response, value: "정제된 Tool 결과", cellStyle: edgeStyle({ strokeColor: COLORS.success, exitX: 0, exitY: 0.5, entryX: 1, entryY: 0.5 }), points: [[950, 945], [270, 945]], }); page.vertex({ x: 330, y: 790, w: 555, h: 110, value: "보안 불변조건
• Token 원문은 DB·로그·응답·Git에 저장하지 않음
• SQL 생성 ADMIN과 실행 CB_ORDS를 분리
• 컨텍스트 설정과 보호 객체 SELECT는 반드시 같은 connection
• 권한 또는 메타데이터가 불완전하면 결과를 축소하는 대신 요청/기동 실패", cellStyle: panelStyle({ fillColor: "#FFF7F2", strokeColor: COLORS.oracleRed, align: "left", verticalAlign: "middle", spacing: 10 }), }); return page.result(); } function writeDrawio(library) { const diagrams = [addOverviewPage(library), addSecurityFlowPage(library)]; const mxfile = [ '', '', ...diagrams, "", ].join("\n"); fs.mkdirSync(outputDir, { recursive: true }); const output = path.join(outputDir, "hmm-ai-data-access-architecture.drawio"); fs.writeFileSync(output, mxfile, "utf8"); return output; } const library = readLibrary(libraryPath); const output = writeDrawio(library); console.log(output);