ADR-0032: 마켓플레이스 publish 범위 — 스키마 단위 게이트 + 노출 분기
배경
MCP 도구 마켓플레이스의 publish 게이트(is_publishable_target)가 대상 FQN 의
카탈로그만 보고 판정했다. iceberg 와 gendpg 전체가 allowlist 라 워크스페이스
사설 자산이 전부 publish 가능했다. 착수 전 실측:
| FQN | 변경 전 | 실제 성격 |
|---|---|---|
gendpg.public.data_grants | ✅ 허용 | GenD 내부 PG 메타데이터 |
iceberg.datasets.ds_abc | ✅ 허용 | 학습데이터셋 (workspace_id NOT NULL) |
iceberg.bronze.intel_* | ✅ 허용 | ws-stamped 공유 테이블 |
iceberg.gold.predictions_* | ✅ 허용 | 모델 출력 |
iceberg (1-세그먼트) | ✅ 허용 | 스키마 미확정 |
tpch.tiny.customer | ❌ 거부 | 공개 벤치마크 데이터 |
allowlist 가 의도와 역방향 집합이었다 — 공유해도 되는 것(tpch)을 빼고 공유하면
안 되는 것(내부 PG)을 넣었다. 게이트의 docstring 은 "workspace-private 데이터는
거부한다"고 선언하는데, 판정 단위가 카탈로그라 그 선언을 지킬 수 없었다.
동시에 publish_visibility 의 tenant/public 이 네 곳 모두 isnot(None) 이라
코드상 동의어였다 — publish 한 번이 곧 전 워크스페이스 공개였다. prod 에는 서로 다른
법인(energy-kogas)과 용도 워크스페이스가 동거한다.
지금 고치는 이유: prod 실측 결과 published 도구 0건 / 구독 0건이라 회수 마이그레이션도 고객 통지도 필요 없다. 첫 publish 가 생기는 순간 이 작업은 파괴적 변경이 되고, 고객이 의존한 뒤에는 계약 사건이 된다.
결정
1. publish 게이트는 스키마 단위, 실행 allowlist 와 분리한다
신규 GEND_MCP_PUBLISHABLE_SCHEMAS (catalog.schema CSV, 기본 iceberg.silver).
★ 기존 GEND_MCP_ALLOWED_CATALOGS 를 재사용하지 않는다. 그 값은 도구 실행
게이트를 겸하므로(build_trino_sql 이 ValueError 를 던진다) 좁히면 돌고 있는
정상 도구가 죽는다. publish 는 "이 데이터를 다른 워크스페이스에 넘겨도 되는가"라는
전혀 다른 질문이므로 별도 설정으로 판정한다.
기본값 iceberg.silver 근거: 코드로 전사 공유가 확인되는 유일한 스키마다 — intel
silver asset 들이 workspace_id 를 sentinel 0(전사 공유 fact)으로 넣는다.
부분 FQN(1~2 세그먼트)은 스키마를 확정할 수 없으므로 fail-closed 거부한다. 예전에는
iceberg 한 조각만 와도 통과했다.
2. tenant/public 을 문자 그대로 시행한다
GenD 는 Workspace 가 곧 테넌트인 flat 모델이다. 문서화된 의미를 그대로 적용하면
tenant = 소스 워크스페이스 내부(외부 노출 0), public = 전 워크스페이스다.
신규 테이블도 레지스트리도 필요 없고, body 생략 시 기본값이 tenant 라 자동으로
fail-closed 가 된다. Phase 2 에 테넌트 그룹 레지스트리가 오면 tenant 가 peer 로
확장되는 forward-compat 도 유지된다.
노출 판정은 EXPORTED_VISIBILITIES 단일 상수에서만 파생시켜 네 곳(목록·구독·
디스패처 2)에 적용한다. 목록만 고치면 tool_id 를 아는 ws-admin 이 그대로 구독하고,
디스패처를 고치지 않으면 public → tenant 강등 시 기존 구독이 계속 동작한다.
추가로 GEND_MARKETPLACE_PUBLIC_PUBLISH_ENABLED(기본 true)를 둔다. 온프렘(사내
위임 = tenant 허용)과 SaaS(고객사 간 = 금지)를 같은 코드로 다르게 배포하기 위한
스위치다.
3. 모드 플래그(off/observe/enforce)를 두지 않는다
리포의 세 선례(grant_enforcement_mode, trino_identity_mode, sdk_authz_mode)는
전부 모든 사용자의 읽기 hot path 를 바꾸는 변경이라 잘못된 기본값이 곧 장애였다.
D1 이 바꾸는 것은 admin 전용 쓰기 1개와, 실측상 결과 집합이 비어 있는 조회·구독
2개다 — 단계 시행으로 관측할 트래픽 자체가 없다.
게다가 off 가 카탈로그 단위 판정을 되살리는 플래그는 "안전하지 않은 동작으로
되돌리는 스위치"가 되어, 이 PR 이 제거하려는 fail-open 과 같은 성질을 새로 만든다.
롤백 레버는 더 안전한 형태로 이미 있다 — allowlist 에 스키마를 추가하면 배포 없이
넓혀진다. 단계 모드가 줬을 관측치("기본 allowlist 가 과도하게 좁은가")는
gend_mcp_marketplace_publish_total{result="rejected_target"} 가 직접 답한다.
4. 유향 2단 동의는 후속 PR — 단 형태를 지금 확정한다
신규 컬럼이 아니라 신규 테이블 workspace_mcp_tool_publications
(tool_id, target_workspace_id, created_by, created_at, UNIQUE) 로 한다.
근거: 신규 테이블은 Base.metadata.create_all 로 생성되어 _COLUMN_MIGRATIONS 등록이
불필요하다(선례 — workspace_mcp_tool_subscriptions/_grants 가 alembic 리비전 0건으로
출시됐다). 반대로 컬럼 추가는 prod 가 alembic 을 돌리지 않으므로 alembic 리비전과
_COLUMN_MIGRATIONS 를 둘 다 넣어야 하고, 보안 좁히기 릴리스에 prod DDL 리스크를
얹게 된다.
미루는 것이 안전한 이유: 이번 PR 이 이미 급한 노출을 닫는다(기본 tenant 는 아무
데도 안 가고 public 은 배포 스위치 뒤에 있다). prod 는 published 0 / 구독 0 이라
미루는 동안 열려 있는 창이 없다.
결과
- 내부 PG 메타데이터·학습데이터셋·DataMart 출력·bronze/gold 가 publish 대상에서 빠진다.
tenant게시물이 실제로 타 워크스페이스에 노출되지 않는다.public → tenant강등 시 기존 구독이 회수된다(플래그 쓰기와 같은 트랜잭션).- 스키마 allowlist 로 못 막는 잔여 구멍(DataMart 는
target_schema자유 입력)은 publish 시 DataMart 등록 조회로 닫는다.
이 결정이 닫지 않는 것
| 갭 | 사유 |
|---|---|
| 유향 2단 동의 | 후속 PR (형태만 확정) |
| unsubscribe 엔드포인트 부재 | 유향 동의 PR 과 함께 설계 |
플랫폼 admin 의 scope_query 전면 우회 | D2(break-glass) 영역 |
| CSV 적재 테이블의 워크스페이스 귀속 표시 부재 | 물리 행 단위 표시가 없어 스키마 allowlist 로만 방어 — 별도 설계 |
| 구독 실행 경로의 Trino 펜스 부재 | 별도 이슈 |
운영
승격·롤백 절차는 marketplace-publish-scope.