# task-2847 보고서 — consultation_history_v1 route-only 강제 (service-role fail-closed + forward migration 015)

- 작업 ID: task-2847 (Lv.4, 개발2팀 오딘)
- 프로젝트: InsuRo (`/home/jay/projects/InsuRo`)
- 브랜치: `task/task-2847-dev2` (base=origin/main `50bb4d8`) · worktree: `/home/jay/projects/InsuRo-worktrees/task-2847-dev2`
- merge_policy: **none** — 머지는 ANU 원격 독립검증 후 소유

## Situation
`consultation_history_v1` 는 route 계층에 feature-flag + pilot allowlist + IDOR 게이트가 있으나, 013 migration 의 RLS 정책이 raw 테이블에 `auth.uid()=fa_account_id` 로 authenticated 직접 SELECT/INSERT 를 허용 → 원리상 route 우회 가능. 파일럿 전 route-only 강제 필요.

## Complication
- Codex 최고위험: 서버 라우트 클라(`main._get_supabase_client` = `SERVICE_ROLE_KEY or ANON_KEY`)가 anon fallback → RLS 를 먼저 잠그면 service-role 미보장 시 서버 자체 insert 가 깨진다. **RLS 잠그기 전에 service-role fail-closed 선차단** 필요.
- 013/014 는 이미 live DB 적용 → 편집 무효. **신규 forward migration** 만 유효.
- B2 계약테스트 3파일(`test_consultation_history_v1.py`/`_get.py`/`_allowlist.py`)은 forbidden(수정 금지).

## Question
전역 클라(40여 라우트 공유·anon fallback 유지)를 건드리지 않고, 이 라우트만 service-role 강제 + RLS route-only 를 어떻게 회귀 0 으로 달성하나?

## Answer (구현)
### 설계 결정 — "별도 admin 클라" → **"service-role 가드"** 전환
초기 구현은 라우트 전용 `_get_supabase_admin_client`(service-role 전용) 도입이었으나, 이 방식은 라우트의 DB 접근 심볼을 바꿔 **기존 B2 계약테스트 39개를 회귀**시켰다(기존 테스트가 `patch.object(main, "_get_supabase_client", ...)` 로 mock → 라우트가 다른 심볼을 호출하면 mock 이 빗나가 실 fail-closed 500). task 지시서가 명시적으로 허용한 **"가드"** 옵션으로 전환하여, 라우트는 기존 `_get_supabase_client` seam 을 유지하고(기존 mock 계속 유효) service-role 부재를 fail-closed 하는 가드 dependency 를 라우트에만 부착 → 회귀 0 + 다른 라우트 영향 0 동시 달성.

### 변경 파일 (4개, 687 insertions, 전부 allowed_resources 내부)
1. `server/routes/consultation_history_v1.py` (+30): 신규 가드 `_require_service_role_backend()` — `INSURO_NEW_SUPABASE_URL` 설정됨 ∧ `INSURO_NEW_SERVICE_ROLE_KEY` 미설정 → `500 SERVICE_ROLE_REQUIRED` (anon fallback 금지). URL 미설정(테스트에서 클라 mock)이면 미개입. 3개 엔드포인트(POST ingest / GET list / GET detail)에 `_feature_check` 다음 `_service_role_check` Depends 배선.
2. `server/migrations/015_consultation_history_v1_rls_route_only.sql` (신규, 26): 013 permissive 정책 2개 DROP(`FA can read/insert own consultation history v1`) + RLS ENABLE 유지(정책 없음=default-deny) + authenticated/anon REVOKE(이중 방어). service_role 은 REVOKE 안 함(BYPASSRLS → 서버 경로 유지). ok-only 뷰 정의 무변경. 멱등(IF EXISTS/재적용 안전).
3. `supabase/migrations/20260723120000_consultation_history_v1_rls_route_only.sql` (신규, 26): 015 미러(내용 동일).
4. `server/tests/test_consultation_history_rls_route_only.py` (신규, 605, 16 테스트): (a)가드 fail-closed 4종[url+role미설정→500 / anon있어도→500 / role설정→통과 / url미설정→미개입] (b)015 SQL 정적 계약 7종[DROP 2·REVOKE authenticated·anon·service_role REVOKE없음·RLS ENABLE·IF EXISTS·미러동일·013무변경] (c)정상흐름 2종 (d)기존게이트 회귀 3종.

★ `server/main.py` 는 원복되어 최종 diff 에 없음(잔여 변경 0) — 가드는 라우트에만 존재, 전역 클라 anon fallback 불변.

## 테스트 결과
- goal_assertion `pytest tests/test_consultation_history_rls_route_only.py` → **16 passed**
- consultation 전체 회귀 → **117 passed** (신규 16 + 기존 101, 회귀 0)
- 기존 B2 계약테스트 3파일(미수정) → 101 passed 유지

## L1 스모크테스트 결과
- 서버 재시작: 해당없음(feature flag default-OFF backend 변경 — 프로덕션 동작 flag ON 전까지 불변)
- API 응답 확인: **FastAPI TestClient 실요청 통과** — `test_a1_service_role_required_when_url_set_and_service_role_missing`(500 SERVICE_ROLE_REQUIRED), `test_a2_...even_when_anon_key_present`(anon 있어도 500), `test_a3_guard_passes_when_service_role_configured`(200), `test_a4_guard_does_not_interfere_when_url_fully_unset`(200), `test_feature_disabled_returns_403_before_service_role_guard`(403) — 마운트된 앱에 실제 HTTP 요청으로 가드·게이트 순서·fail-closed 동작 실증.
- migration live-apply: 해당없음(service-role + live DB 필요 → ANU 원격 독립검증 영역). 015 멱등성·계약은 정적 계약테스트 7종으로 검증.
- 스크린샷: 해당없음(백엔드/migration 작업, UI 없음)

## 발견 이슈 및 해결
1. **admin-client 설계가 B2 테스트 39개 회귀** → 원인(기존 mock seam 이탈) 규명 후 가드 방식으로 전환하여 해결(회귀 0 복원). ambient env 에 `INSURO_NEW_SUPABASE_URL` 부재 확인으로 가드의 테스트 투명성 사전 검증.
2. Pyright "Import main/schemas could not be resolved" 및 편집 중간 스냅샷 경고 → 기존 전 테스트와 동일한 sys.path 런타임 import 패턴 + 편집 중간 상태 잔상, 실제 파일 grep·117 passed 로 무해 확인.

## 머지 판단
- 머지 필요: **Yes** (단, merge_policy=none → ANU 원격 독립검증 후 머지)
- 브랜치: `task/task-2847-dev2`
- 워크트리: `/home/jay/projects/InsuRo-worktrees/task-2847-dev2`
- 머지 의견: 스코프 4파일 전부 allowed_resources 내부, forbidden 0건 접촉. 117 passed 회귀 0. 라우트 국소 변경으로 다른 라우트 영향 0. RLS live-apply 검증만 ANU 영역으로 잔여.

## 모델 사용 기록
- 토르(백엔드, sonnet): main/route/migration 구현 + 가드 전환
- 헤임달(테스터, sonnet): RLS route-only 신규 테스트 작성 + 가드 방식 재작성
- 오딘(팀장, opus): 설계·검토·통합·회귀 진단(admin→가드 전환 판단)
- haiku 미사용

## 세션 통계
- 총 도구 호출: 0회


## 세션 통계
- 총 도구 호출: 0회


## 세션 통계
- 총 도구 호출: 0회

