GitOps 저장소도 ArgoCD Application도 아직 없어, 그때까지 이 저장소가
push 방식 파이프라인(.gitea/workflows/)을 임시로 소유한다. 무엇을
포기하는지와 넘길 때 할 일은 deploy/README.md에 적었다.
Chart는 portal과 bundles 두 배포 모델을 모두 렌더링한다. ADR-0013이
ADR-0007을 대체했으므로 운영은 portal이 기준이지만, bundles 경로를
언제 삭제할지는 아직 정하지 않았다.
- .gitea/workflows/ci.yaml, deploy-openshift.yaml
- deploy/ci/render-manifests.sh, deploy/examples/
- Chart: mode 분기, selectedDeployment/tier helper, imagePullSecrets,
toolService.apiKeySecret 참조
- extension-points.md에 미결 항목 9~12 추가
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
README와 계약 v0.3이 employee-no·virtual-employee-no를 "암호화된 사원번호"로
적고 MCP가 복호화하지 않는다고 설명했다. 암호화 여부는 MCP가 확인할 수 없고
계약이 요구하는 것도 아니므로, MCP 관점에서 참인 것만 남긴다. 해석하지 않고
그대로 전달하는 불투명 값이라는 사실이다.
로그 규칙은 그대로 둔다. 로그에는 guid와 x-request-id만 남기고 사원 식별자는
기록하지 않는다.
IntelliJ 마크다운 포맷터가 표 정렬과 줄바꿈을 함께 정리해 diff가 크다.
서식 외의 실제 변경은 위 두 가지와 운영 설정 절의 미정 표기 추가다.
남은 정리: ADR-0006은 전제 2에서 여전히 "암호화되어 전달되고 복호화 키는
사내 KMS에서 발급받는다"고 적고 있어 이 변경과 어긋난다. ADR의 전제를 바꾸는
것은 별도 결정이므로 이 커밋에 포함하지 않는다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
MCP Server와 Tool Service가 같은 스택과 같은 MCP SDK를 쓰므로 SBOM을
공통으로 적용한다. 산출물 이름을 AXHUB_MCP_Tool_Service_SBOM으로 바꾸고,
목록이 MCP Server 빌드 하나에서 나왔다는 한계를 함께 적는다. Tool Service가
스택 밖 의존을 추가하면 그 빌드에서 다시 산출해 합쳐야 한다.
JDK 라이선스를 미확정에서 GPL-2.0 with Classpath Exception으로 확정한다.
toolchain에 vendor를 고정하면서 배포판이 정해졌기 때문이다. Classpath
Exception이 있어 이 JDK로 실행하는 애플리케이션에 소스 공개 의무는 없다.
inputSchema 관련 보안 통제 설명은 SBOM의 범위가 아니므로 뺀다. 근거는
ADR-0011과 ADR-0012가 소유한다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
vendor를 적지 않으면 설치된 아무 JDK 21이나 잡히므로, 표준가이드가 정한
배포판(openjdk21u-jdk_x64_windows_hotspot_21.0.5)과 다른 것으로 조용히
빌드될 수 있다. SBOM의 JDK 항목도 그 전제 위에 있다.
폐쇄망에서는 toolchain 자동 다운로드가 동작하지 않으므로 빌드 머신에
Temurin이 미리 설치되어 있어야 한다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
main이 6078852의 endpoint 소유권 반전을 ADR-0010으로 기록하면서 이
브랜치의 ADR-0010과 번호가 겹쳤다. 두 문서는 다른 결정이므로 나중에
문서를 합칠 때 한쪽을 옮겨야 한다.
이 브랜치가 0012까지 쓰고 있어 0011·0012를 건드리지 않는 첫 번호인
0013을 쓴다. 파일명과 제목, 대체 관계를 가리키는 ADR-0007·ADR-0009,
결정 목록, architecture 문서, Portal 계약 문서, Helm 설명, 그리고
application.yml 주석의 참조를 함께 바꾼다. 결정 내용은 바뀌지 않는다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
build.gradle이 직접 선언한 오픈소스는 mcp-json-jackson2 하나지만, 런타임
전이 의존까지 따라가면 12건이 산출물 classpath에 올라간다. json-schema-validator,
itu, jackson-dataformat-yaml, reactor-core, reactive-streams는 SDK를 넣기
전에는 없던 것들이라 함께 수록한다. 여기에 빌드 환경 2건(JDK 21, Gradle
8.14.3)을 scope optional로 더해 14건이다.
버전과 해시는 Gradle 로컬 캐시의 실제 pom을 따라가 그래프를 만들고 실제 jar
바이너리에서 SHA-512/SHA-1을 계산했다. Gradle 배포본의 SHA-256은 wrapper의
distributionSha256Sum 값이다. 산출 근거와 한계는 docs/sbom/README.md에 있다.
라이선스는 Apache-2.0 9건, MIT 3건, MIT-0 1건, JDK 미확정 1건이다. copyleft가
없어 소스 공개 의무는 없다. JDK는 toolchain이 벤더를 고정하지 않으므로 실제
배포판이 정해지면 라이선스를 확정해야 한다.
optional인 joni/graal-js/graal-sdk(약 50MB), provided인 jakarta.servlet-api,
test scope와 annotationProcessor는 산출물에 포함되지 않아 제외했다. 제외 사유는
Exclusions 시트에 있다.
확인이 남은 항목은 jackson-databind다. SDK가 요청한 2.20.1이
io.spring.dependency-management에 의해 Spring Boot 3.5.11의 2.19.4로 내려간다.
sandbox에서 gradle daemon이 뜨지 않아 gradlew dependencies로 대조하지 못했으므로
빌드 환경에서 한 번 확인해야 한다.
xlsx가 줄바꿈 정규화 대상이 되지 않도록 .gitattributes에 binary로 명시한다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
MCP Java SDK 도입으로 들어온 json-schema-validator는 schema의 $ref가 문서
밖을 가리키면 그 주소로 직접 조회하고, pattern 검증을 백트래킹 기반
java.util.regex로 처리한다. inputSchema는 Tool Service 매니페스트에서 오므로
매니페스트가 서버의 outbound 대상과 CPU 소비를 정할 수 있었다. AGENTS.md의
"outbound 주소는 설정에서만 온다"는 불변식이 이 경로에서 뚫려 있었다.
DefaultJsonSchemaValidator는 SchemaRegistry를 생성자 안에서 만들고 private
final로 들고 있어 정책 주입 지점이 없다. 따라서 SDK 밖에서만 막을 수 있다.
검사는 ToolMetadata의 표준 생성자에 둔다. Portal 매니페스트, local 파일,
Redis snapshot 역직렬화가 모두 이 생성자를 지나므로 우회 경로가 생기지 않는다.
위반은 기존 매니페스트 형식 오류와 같게 다뤄 bundle 단위 실패 격리와
"Redis 실패는 언제나 cache miss" 동작을 그대로 유지한다.
정규식 규칙은 JDK 21.0.11 실측으로 정했다. 통념과 달리 (a+)+는 빠르게 끝나고,
중첩이 아닌 a*a*a*a*a*b와 바깥 반복이 유한한 (.*,){11}P가 폭증했다. 겹치는
문자 집합 판정은 결정 불가능하므로 모양 검사만으로는 부족하고, pattern 필드에
maxLength 동반 선언을 요구해 입력 길이를 묶는 것이 실질적인 상한이 된다.
patternProperties는 key에 길이를 선언할 자리가 없어 사용을 금지한다.
format은 단언되지 않아 format:regex 경로가 실행되지 않는다는 사실도 계약
테스트로 고정했다. SDK 업그레이드로 단언이 켜지면 테스트가 실패한다.
Tool Service는 pattern을 쓰는 필드에 maxLength(<=256)를 선언해야 하므로
매니페스트 수용 조건이 바뀐다. Tool Service 파트와 합의가 필요하다.
근거: ADR-0011, ADR-0012
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
.ua/는 understand 스킬의 knowledge-graph 캐시이고 skills-lock.json은
스킬 매니저가 만드는 잠금 파일이다. 둘 다 로컬 도구 산출물이라
저장소가 소유할 대상이 아닌데 매번 untracked로 남아 status를 흐렸다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
내부망 운영은 route↔Tool Service 매핑을 Portal이 소유하고, MCP 배포 하나가 N개
route를 서비스하며, route 하나에 N개 Tool Service가 붙을 수 있다. 이 판단을
ADR-0010으로 남기고 ADR-0007 전체와 ADR-0009 결정 4를 대체한다.
계약
- Portal-MCP registry 조회 계약 v0.1과 예제 JSON을 docs/contracts/portal-mcp/에
신설한다. 지금까지 이 경로에는 정본이 없었다.
- 예제를 PortalToolRegistryClient의 실제 파싱 경로에 태우는 계약 테스트를 추가해
문서와 구현이 따로 표류하지 않게 한다.
실패 격리
- fetchAllTools()의 실패 전파를 route 단위로 격리한다. 계약 v0.2의 "aggregate는
전부 아니면 전무"는 카탈로그 하나를 전제한 규칙인데, route가 N개가 되면서 전
route로 확대돼 있었다. Tool Service 하나의 장애가 cold start에서 Pod 전체를
내리고 steady state에서 모든 route의 갱신을 멈추던 동작을 없앤다.
- 제거 판단의 원천을 ToolRegistryClient.knownRoutes()로 분리한다. 조회 결과를
기준으로 지우면 이번 주기에 실패한 route의 정상 snapshot까지 사라져 "어떤
실패도 목록을 비우지 않는다" 불변식이 깨진다.
- readiness는 최소 1개 route로 UP을 유지한다. 모든 route를 요구하면 정상 route까지
트래픽에서 빠져 위 격리를 되돌리기 때문이다. 대신 routesWithoutSnapshot을
health detail로 노출해 관제가 부분 상태를 감지하게 한다.
설정과 기동
- warm start가 route별 Redis key를 읽도록 확장하고, 읽을 key를 알기 위해 기동
preload 순서를 registry 조회 → warm start → manifest 조회로 바꾼다.
- 어떤 코드도 읽지 않던 mcp.portal.route-key를 제거한다. route key는 요청 URI에서만
결정되며, 설정으로 보정하면 잘못된 단일 진입점 호출이 조용히 성공한다.
정리
- ToolRegistryService.java의 이중 인코딩으로 깨져 있던 한글 Javadoc 33줄을 코드
동작에 맞춰 다시 쓰고, replaceSnapshot 위에 겹쳐 있던 고아 Javadoc 블록을 지운다.
- Helm chart는 mcp.bundles 구성에서 계속 유효하므로 삭제하지 않고, 내부망 운영
대상이 아니라는 사실을 deploy/README.md와 values.yaml에 명시한다.
검증: 이 환경은 loopback이 막혀 gradlew check를 실행하지 못했다. CodeStyleContract가
보는 항목(줄바꿈, 탭, 행말 공백, 파일 끝 개행, 미사용 import, import 순서)은 변경된
Java 13개 파일에 대해 따로 재현해 확인했다. 컴파일과 테스트 실행은 미확인이다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>