본문으로 건너뛰기

ADR-0032: 마켓플레이스 publish 범위 — 스키마 단위 게이트 + 노출 분기

  • 상태: 승인
  • 날짜: 2026-07-30
  • 관련: #2710 (본 결정), #2700 (워크스페이스 격리 Epic, 결정 D1), #2047 (마켓플레이스 출시), ADR-0031

배경

MCP 도구 마켓플레이스의 publish 게이트(is_publishable_target)가 대상 FQN 의 카탈로그만 보고 판정했다. iceberggendpg 전체가 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_visibilitytenant/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_sqlValueError 를 던진다) 좁히면 돌고 있는 정상 도구가 죽는다. 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.