# task-2851 보고서 — 확장 배포 배선(host api.insuro.biz + manifest key + 파일럿 flag 아티팩트)

- 작업 ID: task-2851 (Lv.3, TASK-EXT-PLUMBING)
- 팀: dev1-team (헤르메스 팀장 / 불칸 백엔드 / 아르고스 테스터)
- 프로젝트: InsuRo
- worktree: `/home/jay/projects/.worktrees/InsuRo-task-2851-dev1`
- 브랜치: `task/task-2851-dev1`
- base: origin/main `60b645d` (계약정합 #124 + RLS #123 반영됨)
- merge_policy: **none** — 검증·머지는 ANU(개발실장) 원격 소유

---

## S (Situation)
계약 정합·RLS 머지 완료. 파일럿(회장 flag-on) 직전, 확장이 서버에 실제로 도달하지 못하는 배포 배선 3건이 실측으로 확정됨:
- 확장 ingest/CRM egress 가 `https://insuro.biz`(프론트, 405)로 감. 진짜 API=`https://api.insuro.biz`(→uvicorn:8001, curl 403 FEATURE_DISABLED 실증).
- allowlist(`isAllowedIngestUrl`/`isAllowedCrmUrl`)가 `hostname==="insuro.biz"` 하드코딩 → host만 바꾸면 egress 자기차단.
- manifest 에 `key` 없음 → 로드마다 확장 ID 변동 → 서버 CORS(`INSURO_EXTENSION_ORIGIN`) 고정 불가.
- 파일럿 flag `INSURO_CONSULTATION_HISTORY_V1_ENABLED=false` 하드코딩 + 번들러 없음(importScripts plain JS).

## C (Complication)
host 상수 하나(`insuro.biz`)가 소스 2파일 + 다수 테스트 파일과 결합. 단순 치환이 아니라 "allowlist 가 api.insuro.biz 허용·insuro.biz(프론트) 거부"라는 새 진실을 소스·테스트 양쪽에 정합해야 하며, DEFAULT URL 변경이 background-dispatcher/egress-gate 의 happy-path 픽스처(`ingest_url`)와 exact-match assertion 을 깨뜨림. 회귀 0 유지가 관건.

## Q (Question)
소스를 왜곡하지 않고 host/key/flag 를 정확히 배선하면서, 결합된 전체 테스트를 회귀 0로 통과시킬 수 있는가? 소스 flag 는 false 로 두고 파일럿 빌드만 true 로 만드는 아티팩트 경로를 만들 수 있는가?

## A (Answer) — 구현 및 검증

### 수정/생성 파일 (변경 10개, 전부 allowed_resources 내부)
1. `extension/background/ingest.js`
   - `INGEST_PROD_URL`(41): `https://insuro.biz` → `https://api.insuro.biz` (+ INGEST_PATH)
   - `isAllowedIngestUrl` isProdHost(137): `hostname==="insuro.biz"` → `"api.insuro.biz"`
   - 기존 egress 게이트 로직(protocol/pathname/userinfo/localhost) **불변**. flag(38) `false` **유지**.
2. `extension/background/crm.js`
   - `CRM_PROD_ORIGIN`(20): `https://insuro.biz` → `https://api.insuro.biz`
   - `isAllowedCrmUrl` isProdHost(69): `hostname==="insuro.biz"` → `"api.insuro.biz"`
   - 포트(`""||"443"`)/search·hash 거부/localhost 로직 **불변**.
3. `extension/manifest.json`
   - 최상위 `"key"` 추가(RSA2048 DER 공개키 base64, len 392)
   - `host_permissions` 에 `"https://api.insuro.biz/*"` 추가. 기존 `"https://insuro.biz/*"`(externally_connectable/content_scripts JWT 브리지·프론트용) **유지**.
4. `scripts/build-pilot-extension.mjs` (신규)
   - `extension/` → `dist-pilot/extension` 복사 후 복사본 ingest.js flag `false`→`true` 정확 1건 치환(0/2+건 시 exit 1). **소스 원본 불변.**
   - `node:fs`/`node:path` ESM, 외부 의존 0. `node scripts/build-pilot-extension.mjs` 실행.
5. `.gitignore` — 루트에 `dist-pilot/` append (아티팩트 커밋 방지).
6. `extension/__tests__/` (5파일: prd1-ingest-core, prd2-crm-core, a7-write-path-blocked, prd1-background-dispatcher, prd2-crm-egress-gate) — api.insuro.biz 허용/insuro.biz(프론트) 거부 반영, 프론트 host 차단 **신규 거부 케이스**(ingest/crm 각 1건) 추가.

### ★ 확장 ID (OPS-2 CORS env 용 — 필수 명시)
```
chrome-extension://ekhhgeekdelijnhkjbclpoomblbpgplj/
```
- 산출: `SHA256(DER 공개키)` 첫 16바이트 → 각 hex nibble(0–f)을 a–p 로 매핑(Chrome 표준 알고리즘).
- ANU 는 이 값을 서버 CORS env(`INSURO_EXTENSION_ORIGIN=chrome-extension://ekhhgeekdelijnhkjbclpoomblbpgplj`)에 사용.
- 아르고스가 manifest key 로부터 **독립 재계산 → 동일값 확인**(match: true).

### 커밋 (worktree 브랜치)
- `d5afcd0` — api.insuro.biz host 교체(ingest/crm) + manifest key/host_permissions + 파일럿 빌드 스크립트
- `74d5830` — 확장 테스트 정합 + 프론트 host 차단 신규 거부 케이스

---

## L1 스모크테스트 결과 (필수 기록)
- 서버 재시작: **해당없음** — 이 작업은 확장 소스/manifest/빌드 스크립트 배선(서버·content.js 불변). 서버 기동 대상 아님.
- API 응답 확인: **해당없음(서버 미접촉)**. 대신 egress allowlist 함수를 **직접 실동작 호출**로 실증(아래).
- 파일럿 스크립트 L1 실행: **성공** — `node scripts/build-pilot-extension.mjs` EXIT=0, "patched flag = true (1 replacement)". 산출물 `dist-pilot/extension/background/ingest.js:38` = `true`, 소스 원본 `:38` = `false` (grep 대조 증명).
- allowlist 실동작(아르고스 독립 CJS 하네스 — `"type":"module"` 환경이라 plain require 대신 CJS vm 하네스로 함수 직접 호출):
  - `[ingest] api.insuro.biz 허용` → true ✅ / `insuro.biz(프론트) 거부` → false ✅ / `localhost:8000 허용` → true ✅
  - `[crm] api.insuro.biz 허용` → true ✅ / `insuro.biz 거부` → false ✅ / `api.insuro.biz:444 포트거부` → false ✅ / `api.insuro.biz?q=1 쿼리거부` → false ✅
- 스크린샷: 해당없음(브라우저 UI 변경 없음, 서비스워커/manifest 레벨 배선).

## 테스트 결과
- `npx vitest run extension/__tests__/`: **18 파일 / 394 tests 전부 PASS, FAIL 0** (baseline 392 → 394, 신규 거부 케이스 2건 추가). 회귀 0.
- `tests/contract/`: vitest 대상 파일(.test/.spec) 없음(generator + golden fixture만) → 스킵. 계약 골든 회귀는 `extension/__tests__/contract-golden.test.ts`(7 tests PASS)가 커버. golden 은 host-agnostic canonical body 라 host 리터럴 없음(변경 불요).
- manifest JSON 유효성: `JSON.parse` OK, key len 392, host_permissions 에 api/apex 양쪽 존재.

## 독립 검증(아르고스) 종합
V1(회귀 394 PASS) · V2(allowlist 7/7) · V3(파일럿 flag 소스 false/산출 true) · V4(확장 ID 재계산 일치) · V5(content.js/src/server/.github 미접촉, 변경 전부 allowed_resources 내부) — **전 항목 PASS**.

## 발견 이슈 및 해결
- **진단(diagnostics) 경고**: ingest.js:179 `consent` 미사용, prd2-crm-core:24 `createOpaqueValue` 미사용, a7:454/608 tuple 타입 nit — `git diff 60b645d` 대조 결과 **전부 base(60b645d)에 이미 존재하던 pre-existing TS strictness 경고**(불칸이 파일을 건드려 표면화됨). vitest 는 esbuild 트랜스파일로 타입체크 안 함 → 394 PASS. host/key/flag 배선 범위 밖 + 추가 테스트 라인 수정은 회귀 위험 → **손대지 않음**(scope 준수).
- **node_modules 부재**: worktree 에 node_modules 없어 vitest 실행 불가 → 메인 레포 `/home/jay/projects/InsuRo/node_modules` 심링크(gitignore됨, 미커밋)로 해결.
- **`"type":"module"` require 이슈**: 루트 package.json 이 ESM 이라 plain `node require("./ingest.js")` 는 export 빈 객체 반환(코드 결함 아님 — module.exports 가드는 정상, vitest CJS interop 은 정상 로드). 아르고스가 동등 CJS vm 하네스로 우회 실증.

## 버그 유무
- 신규 버그 없음. 소스 왜곡 없음. 소스 flag=false 유지(프로덕션 절대 안 켜짐), 파일럿 아티팩트만 true.

---

## 머지 판단
- **머지 필요**: Yes (단, merge_policy=none → **ANU 원격 검증·머지 소유**. 팀장 자체 머지 안 함.)
- **브랜치**: `task/task-2851-dev1`
- **워크트리 경로**: `/home/jay/projects/.worktrees/InsuRo-task-2851-dev1`
- **머지 의견**: 394 tests PASS(회귀 0), allowlist 실동작 실증(api 허용/프론트 거부), 소스 불변 범위 준수(content.js/서버 미접촉), 확장 ID 결정론 확인. 변경 전부 allowed_resources 내부. 계약 골든 유지. 안전하게 머지 가능.
- **ANU 후속(OPS)**: ① 서버 CORS env `INSURO_EXTENSION_ORIGIN=chrome-extension://ekhhgeekdelijnhkjbclpoomblbpgplj` 설정 ② 서버 stash+pull+build+restart(stale 최신화) ③ 파일럿 배포 시 `node scripts/build-pilot-extension.mjs` 산출물(`dist-pilot/extension`) 로드 ④ G-3 GREEN → 회장 flag-on.

### 확장 키/ID 재현성 노트 (OPS 참고)
- manifest `"key"`(공개키 DER)는 커밋됨 → **unpacked 로드**(dist-pilot/extension 을 chrome://extensions 개발자모드 로드) 시 확장 ID `ekhhgeekdelijnhkjbclpoomblbpgplj` 로 고정. **개인키 불필요**(파일럿 1차 경로).
- 서명된 `.crx` 를 **동일 ID 로 패킹**해야 할 경우에만 개인키 필요. 개인키 보존 위치(레포 밖·미커밋): `/home/jay/.insuro-ext-keys/task-2851-priv.pem` (perm 600). 이 개인키로 재산출한 ID = 커밋된 값과 일치 검증됨. ★ 재생성 시 ID 가 바뀌어 CORS env 가 깨지므로 이 개인키를 사용할 것.
- ★ 보안: 개인키는 리포에 **미커밋**(공개키 DER 만 manifest 에). 하드코딩된 시크릿 0.

### finish-task 운영 노트 (재현/디버깅용)
- merge_policy=none → `FINALIZE_ONLY=1 G4_GATE_ENABLED=0 FINISH_TASK_WORKTREE_STRICT=1` + PROJECT_PATH=**worktree**(메인 레포 아님)로 finish-task 실행. 메인 레포(`/home/jay/projects/InsuRo`)에 task 무관 pre-existing 24 staged 파일이 있어 PROJECT_PATH 를 메인 레포로 주면 `file_touch_ratio_check(NO_UNCOMMITTED)` 오탐 FAIL → worktree 로 지정해 해소(worktree clean).
- p0b_inbox result.json 이 프로젝트 경로(`/home/jay/projects/InsuRo/memory/events/p0b_inbox/`)로 빗나가 workspace canonical(`/home/jay/workspace/memory/events/p0b_inbox/`)로 cp 함(OS-level pickup runner 가 canonical 을 읽음). callback_schedule_created 미자가등록·authoritative=false·owner_pickup_required=true → ANU 독립 검증·머지·closeout 소유.

## 모델 사용 기록
- 헤르메스(팀장): Opus — 설계/분배/검토/통합/키 생성(setup)만. 직접 코딩 없음.
- 불칸(백엔드): general-purpose(sonnet 계열) — 소스 배선 + 테스트 정합 + 커밋.
- 아르고스(테스터): general-purpose(sonnet 계열) — 독립 회귀/allowlist/파일럿/ID/불변 검증.
- haiku 미사용.

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

