# task-2932 — [InsuRo P0-1] Drive 프록시 Phase 3: 공개 완전 차단 (신규 제거 + 소급 회수 스크립트)

## Situation
InsuRo P0-1 Drive 프록시 Phase 1/1.5/2 완료·배포(file-ticket + 소유자 OAuth 스트리밍 프록시 + 프론트 8곳 배선). Codex 확인: 프론트 직접링크 잔존 0, 서버 소유자 OAuth 전환 완료 → 공개(anyone) 완전 제거 안전. 본 Phase 3로 유출 차단 목표 달성.

## Complication
신규 업로드는 여전히 Drive `anyone` 공개 권한을 생성(유출 표면 상존), 기존 업로드분도 공개 상태로 잔존. 단, **FCPA(금소법 PDF)는 회장 결정=공개 유지** → 소급 회수에서 반드시 제외해야 하는 예외.

## Question
(1) 신규 anyone 생성을 안전하게 제거하되 기존 업로드 기능 회귀 0을 어떻게 보장? (2) 기존 공개파일을 FCPA만 정확히 제외하고, `type:anyone`만 선별 회수하며, 실수 실행을 구조적으로 막는 스크립트를 어떻게?

## Answer

### Phase 3a — 신규 anyone 생성 제거 (`server/gdrive.py`)
- `upload_pdf`(구 141~144행)·`upload_file_to_drive`(구 196~200행)의 `service.permissions().create(body={"type":"anyone","role":"reader"})` 호출 블록 **완전 제거**.
- 두 함수 모두 파일 생성/업로드 로직·반환 URL(`https://drive.google.com/file/d/{id}/view`) **무변경**. 신규 업로드는 소유자 전용(비공개) → 접근은 Phase 1 file-ticket 프록시로만.
- `grep -c "anyone" server/gdrive.py` → **0건** (주석 포함 완전 제거 확인).

**인지 검증 노트**: `upload_pdf`는 FCPA(`InsuRo/fcpa/`) 업로더이나, main.py:5916 프록시 인가 로직이 이미 FCPA를 drive_file_id·소유자 OAuth로 서빙(`grade="public"`, 인증사용자 허용)하므로, 신규 FCPA가 비공개여도 프록시로 정상 접근됨. task 배경의 "Codex 확인 공개제거 안전"과 일치. 기존 FCPA 공개파일은 Phase 3b에서 소급 제외(공개 유지).

### Phase 3b — 소급 회수/롤백 스크립트 (실행은 ANU 승인 하)
**`server/scripts/revoke_public_drive.py`** (신규, 332행):
1. **수집**: `newsletters.drive_file_id`(scalar) + `premium_data.drive_file_id`(scalar) + `policy_analyses.drive_file_ids`(배열) 전수. None/빈값 제거 + set 중복 제거.
2. **FCPA 제외 (2중 방어)**: (a) `fcpa_config`를 회수 소스에서 아예 미조회. (b) `collect_fcpa_file_ids()`가 `fcpa_config.drive_file_id`(우선)/`pdf_url→extract_drive_file_id`(폴백) 집합을 별도 조회 → `targets = raw − fcpa_ids` 차집합 제외. 제외분은 `skipped_fcpa`로 리포트.
3. **스냅샷**: 각 file_id `permissions.list` → `type=="anyone"`만 추출하여 `revoke_snapshot_{ts}.json` 저장(롤백 기준). **dry-run/execute 양쪽 모두 생성**.
4. **선별 삭제**: `type=="anyone"` permission만 `permissions.delete`. owner/user/domain 권한 불가침. `--execute`일 때만(`if not execute: continue` 코드 레벨 가드).
5. **안전장치**: `--dry-run`(기본, argparse 상호배타) / `--execute`. rate limit(429/403) 지수 백오프(최대 5회 1→2→4s). 개별 실패 로그 후 계속(전체 중단 없음). 결과 리포트(collected/skipped_fcpa/anyone_found/deleted/failed).

**`server/scripts/restore_public_drive.py`** (신규, 159행): `--snapshot` 기반 `type:anyone` 재부여. 이미 anyone 존재 시 스킵(중복 방지), role 보존, `--dry-run` 기본/`--execute`, 동일 백오프·부분실패 패턴.

## 생성/수정 파일
- `server/gdrive.py` (수정) — anyone 블록 2곳 제거
- `server/tests/test_gdrive.py` (수정) — 기존 2개 테스트를 새 계약(`assert_not_called()` + URL 반환 유지)으로 갱신 (회귀 방지)
- `server/scripts/revoke_public_drive.py` (신규, 332행)
- `server/scripts/restore_public_drive.py` (신규, 159행)
- `server/tests/test_gdrive_no_public.py` (신규, 87행)
- `server/tests/test_revoke_public_drive.py` (신규, 222행)

## 테스트 결과
- **py_compile**: gdrive.py + revoke + restore 3파일 PASS.
- **신규 유닛테스트**: 11 passed (anyone 미생성/URL 반환 회귀, 수집 정확성, FCPA 제외, 스냅샷 type:anyone 필터, execute-only delete, dry-run 삭제0).
- **전체 회귀 스윕(CI parity)**: 내 worktree **1392 passed, 1 failed** / base(b21b04b, .env 복사) **1381 passed, 1 failed**. 유일 실패 = `test_consultation_history_get.py::test_cors_fail_closed_when_ext_origin_unset` — **base 동일 재현**(`.env`의 INSURO_EXT_ORIGIN 존재로 인한 pre-existing 환경 이슈, gdrive/scripts와 무관). **회귀 0** 확정. 차이 +11 = 신규 테스트.

## L1 스모크테스트 결과
- 서버 재시작: **해당없음** (본 작업은 Drive 유틸/오프라인 스크립트, 서버 라우트 무변경. Phase 3a 실동작은 실제 Drive 업로드=네트워크 필요→금지)
- API 응답 확인: **해당없음** (curl 대상 신규 엔드포인트 없음)
- 스크립트 실동작(subprocess 카테고리):
  - `revoke_public_drive.py --help` → argparse 배선 정상(`--dry-run`/`--execute`/`--snapshot-dir` 노출).
  - dry-run 가드 실함수 실행(스텁 MagicMock, 네트워크 0): **dry-run → deleted=0**, **execute → anyone 1건만 delete**(`call(fileId='F1', permissionId='p_any')` 확인). L1 PASS.
- 스크린샷: 해당없음(백엔드/CLI)

## 머지 판단
- **머지 필요**: **No (finalize-only)** — task 지시: "Phase 3a 서버코드 머지·systemd 재배포=ANU / Phase 3b 스크립트는 머지만 / finalize-only". P0-1 스택(2927·2929·2930·2931)과 coordinated 머지+배포.
- **브랜치**: `task/task-2932-dev1` (base=origin/main b21b04b)
- **워크트리 경로**: `/home/jay/projects/InsuRo/.worktrees/task-2932-dev1`
- **머지 의견**: 유닛/회귀/CI-parity/L1 전부 PASS, 회귀 0. MERGE-READY. 단, **실제 `--execute`(소급 회수, 파일 공개 제거)는 절대 실행 안 함** — ANU가 dry-run 실행→스냅샷 확보→회장 최종승인 후 별도 실행. Phase 3a 배포도 P0-1 스택 coordinated 재배포(회장 go + Codex 재조율) HOLD.

## 후속 (ANU)
1. 독립검증: Phase 3a 신규 anyone 미생성 + Phase 3b dry-run/스냅샷/선별삭제/FCPA제외 재확인.
2. Codex 재조율(3b 스크립트 구현 검증).
3. 합의 시 → 3a 머지·systemd `insuro-api` 재배포 + **3b: dry-run 실행→스냅샷 검토→회장 최종승인→실제 소급 회수(`--execute`)**. 문제 시 `restore_public_drive.py --snapshot ... --execute` 롤백.

## 모델 사용 기록
- 불칸(백엔드): sonnet (Phase 3a+3b 구현·테스트). haiku 미사용.
- 팀장(헤르메스, Opus): 설계/인지검증/코드리뷰/회귀 CI-parity 검증/L1 통합 (직접 코딩 없음).

## Sub-agents/횡단조직
- 횡단조직 소환 없음. 아르고스(테스터) 미소환 — 팀장이 직접 기능테스트(G2)+회귀 CI-parity+L1 수행(강한 커버리지로 충분 판단).

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

