---
task_id: "2850"
team: 2팀
level: 2
priority: P1
scope: task
status: completed
depends_on: ["2849"]
created_at: "2026-07-24T08:37:00Z"
deadline: null
---

# task-2850 — consent 진위 수정(granted_at 실 클릭시각) + canary 새 shape + 골든 provenance

## Situation (상황)
task-2849(계약 정합, flat OHMY_PPD_V2)은 골든 파싱에 성공했으나 Codex 코드검토가 3건을 지적:
1. 🔴 **consent 조작**: content.js가 `consent:true`(bool)만 보내고 ingest.js가 bool→consent 객체를 합성.
   `granted_at`이 **조립 시각**(실 동의 클릭 시각이 아님) → 규제 보험 PII 감사 무결성 취약.
2. 🟡 canary sink-spy가 **옛 shape**(reference_type:"unmatched", 옛 matrix) → 새 flat 계약 미검증.
3. 🟡 골든 fixture **provenance 불명**(수기 의심 — 명세는 실산출 요구).

## Complication (문제)
- background.js는 allowed_resources **밖**(수정 불가)이며 `{snapshot, analysis_matrix, consent(bool), reference}`
  4개만 assembleRequestBody로 forward + `consent !== true` 게이트만 통과 → content.js가 consent **객체**를
  전달할 유일 경로는 pass-through 필드(snapshot)뿐.
- _previewSnapshot은 deepFrozen(preview 시점) → 클릭 시점 consent를 원본에 넣을 수 없음.

## Question (질문)
background.js를 수정하지 않고 어떻게 (1) 실 동의 클릭 시각을 캡처해 (2) 서버 ConsentPayload 객체로
전선까지 동결하며 (3) 부재 시 fail-closed 할 것인가?

## Answer (해결)
### 프로덕션 코드 (2 파일 — allowed_resources 내부)
1. **extension/content.js** (프레이야):
   - `handleConsentSaveClick` 최상단에서 `const grantedAt = new Date().toISOString()` — 클릭 = 실 동의 시점.
   - consent 객체 `{granted_at, consent_version:"v1", scope:"consultation_history_ingest", source:"extension_popup"}`를
     구성해 `snapshotWithConsent = {...snapshot, consent:{...}}`(shallow copy) 로 SAVE 메시지 전송.
   - 원본 preview 스냅샷은 불변(track-a-preview 회귀 유지). 상수 `INSURO_CONSENT_VERSION/SCOPE/SOURCE` 추가.
2. **extension/background/ingest.js** (토르):
   - `extractConsentPayload(snapshot)` 신규 — `snapshot.consent`에서 화이트리스트(granted_at/version/scope/source)만
     추출. `consent` 부재→`throw INGEST_CONSENT_MISSING`, `granted_at` 부재/무효→`throw INGEST_CONSENT_GRANTED_AT_MISSING`
     (fail-closed → background가 ASSEMBLE_FAILED). **bool→객체 합성(`new Date()`) 완전 제거.**
   - 시그니처/`CONSENT_*` 상수/exports/`stableStringify` 라인 불변(다른 테스트 anchor 보존).
   - 재시도는 frozen blob 재전송 → granted_at은 동의 시점 값으로 고정(조립/재시도 나중이어도 불변).

### 테스트 (헤임달 — extension/__tests__/**, tests/contract/**)
- **canary sink-spy 새 shape**(핵심 항목2): `markedReference` → `reference_type:"CUSTOMER"`,
  `markedMatrix` → 새 flat `insurers/coverages` shape(PII 마커는 insurers[].name 유지), `markedSnapshot`에 consent 추가.
  → 새 계약 bytes로 PII/JWT 비허용 sink **0건** 실증 + 뮤테이션 대조군(누출 주입 시 실제 검출) 유지.
- **골든 provenance**(핵심 항목3): `tests/contract/generate_golden.mjs` 신규 — `node:vm`으로 ingest.js를 평가해
  실 `assembleRequestBody` 산출을 골든으로 사용(유일 비결정 필드 idempotency_key만 픽스처 고정값 핀).
  `contract-golden-provenance.test.ts` 신규 — 커밋 골든 == `buildGolden()` 산출 **deepEqual 강제**(드리프트 시 실패).
- **fail-closed 회귀**: consent 부재→throw, granted_at 부재→throw 단위테스트 추가.
- 회귀 수정: contract-golden / prd1-ingest-core / prd1-background-dispatcher 의 대표입력·snapshot에 consent 객체 반영.

## 생성/수정 파일
수정(프로덕션 2): `extension/content.js`, `extension/background/ingest.js`
수정(테스트 4): `contract-golden.test.ts`, `prd1-ingest-core.test.ts`, `prd1-background-dispatcher.test.ts`, `prd1-canary-sink-spy.test.ts`
신규(2): `extension/__tests__/contract-golden-provenance.test.ts`, `tests/contract/generate_golden.mjs`
※ 서버/manifest/crm.js/background.js 전부 불변. 골든 JSON 바이트 불변.

## 테스트 결과 (독립 검증 — 팀장 직접 실행)
- 확장 전체 vitest: **19 files / 402 tests PASS** (신규 실패 0, 기준 상회)
- 서버 골든테스트 `server/tests/test_contract_golden.py`: **3 passed** (골든 바이트 불변 → 계약 유지)
- 골든 드리프트: `node tests/contract/generate_golden.mjs` 후 `git diff` **0 라인**
- canary(새 CUSTOMER/flat shape): PII/JWT 비허용 sink 0
- fail-closed(granted_at 부재→throw): PASS

## L1 스모크테스트 결과
- 서버 재시작: **해당없음** (서버 코드 무변경, flag OFF — 전송 경로 미활성, 새 엔드포인트 없음)
- API 응답 확인: **해당없음** (curl 대상 없음 — 본 변경은 확장 클라이언트 조립/동의 로직 + 테스트)
- 실동작 확인(대체): **실행 기반 검증 수행** —
  - `generate_golden.mjs` 가 **실 `assembleRequestBody`**(실 crypto.subtle SHA-256 + TextEncoder bytes)를 vm에서
    구동해 결정론적 골든 바이트를 산출 → 커밋 골든과 byte-동일(수기 아님 실증).
  - canary sink-spy 가 **실 ingest.js + background.js**를 vm 합성 컨텍스트에서 SAVE→RETRY→만료 전 과정으로 구동
    (실 메시지 플로우) → 새 shape에서 PII/JWT sink 0.
  - pytest PASS ≠ 실동작 원칙 준수: 위는 mock이 아닌 실제 소스 모듈 실행 경로.
- 스크린샷: **해당없음** (UI 렌더 변경 없음 — 버튼/상태 문구 불변, 로직만 변경. 실 확장 로드는 flag OFF로 전송 0)

## 머지 판단
- **머지 필요**: Yes (단, **merge_policy=none** — ANU 원격 독립검증·머지 소유)
- **브랜치**: `task/task-2849-dev2` (계약 정합 PR에 이어서 커밋, 새 PR 금지)
- **워크트리 경로**: `/home/jay/projects/InsuRo/.worktrees/task-2849-dev2`
- **push**: `a301e2a..ca3271f` (origin 반영 완료)
- **PR**: 없음(task-2849 계약 정합과 함께 ANU가 검증→계약+RLS 순차 머지)
- **머지 의견**: Codex 3지적 전부 해소. granted_at=실 클릭시각(입력 반영 실증), bool 합성 제거·fail-closed,
  canary 새 shape sink 0, 골든 provenance CI 강제. 확장 402 + 서버 3 PASS, 골든 드리프트 0. 충돌 위험 낮음(scope 내 격리).
  ANU+Codex 재검증(granted_at 실시각·canary·골든 provenance) 후 머지 권장.

## 발견 이슈 및 해결
- **스코프 위반 자가교정**: 초기 헤임달이 `extension/background/package.json`(CJS interop용)을 생성 →
  allowed_resources **밖**(scope-guard 머지차단 유발). 즉시 제거하고 generate_golden.mjs를 `node:vm` 로더로
  자립화(추가 파일 0)하여 standalone `node` 실행·vitest import 양쪽 동작 + 스코프 내부만 변경 유지. 재검증 통과.

## 모델 사용 기록
- 토르(백엔드, ingest.js) / 프레이야(프론트, content.js) / 헤임달(테스터, 전 테스트+생성기): **전원 sonnet**
- haiku 미사용(계약·보안·감사 무결성 관련 코드 — 정확도 우선).
- 팀장(오딘, opus): 설계/분배/검토/독립검증/통합만. 직접 코딩 0.

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


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


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

