# task-2729+13 보고서 — SEALED_ANU_KEY_SUPPLY 감사 + driver main() launcher_fn 결선 (PR_READY_CANDIDATE, ACTIVE=false)

- 작업 ID: task-2729+13 / 팀: dev2-team (오딘) / 레벨: Lv.3
- CODE_ROOT: `/home/jay/p0b-pickup-main` (브랜치 `task/task-2729+13-dev2`, base `0ce319ca`)
- DATA(canonical): `/home/jay/workspace` (무손상)
- 완료 판정: **PR_READY_CANDIDATE_SEALED_KEY_LAUNCHER_WIRING_ACTIVE_FALSE** (PR 미생성, activation 0)

## S — Situation (상황)
P0-b ANU pickup driver는 production entrypoint(`scripts/anu_pickup_entrypoint.sh` → `python3 -m dispatch.anu_pickup_driver`)로 기동되나, `main()`이 `scan_once(CANONICAL_ROOT, legacy_cutoff=True)`만 호출하여 **launcher_fn 미전달** 상태였다(launcher_fn=None → real auto-wake 0, surface only). sealed ANU key 공급(`.env.keys` `COKACDIR_KEY_ANU`)은 이미 구현·동작 중이라는 정정(★) 보고가 전제였다.

## C — Complication (복잡성)
- 이전 'SEALED_ANU_KEY_SUPPLY 미해결' 보고는 grep 패턴 오류(`export ` 접두 미매칭)에 기인한 오탐. 실제로는 `_default_sealed_key_loader()`가 `.env.keys`의 `COKACDIR_KEY_ANU`를 로드하고, 부재 시 `pickup_once`가 `PICKUP_SEALED_KEY_MISSING`로 **fail-closed(wake 0)** 한다.
- 남은 실제 gap은 driver `main()`의 launcher_fn 미결선(gap2) 하나뿐.
- 제약: production activation 금지(systemd enable 0 / ACTIVE=true 0 / activation_epoch 0 / 추가 real ANU spawn 0 / raw key 0 / canonical 무손상). EXPECTED FILES 정확히 5.

## Q — Question (질문)
세션 임시 key 의존 없이 sealed key 기반으로 launcher_fn을 결선하되, **real-wake activation flag가 있을 때만** real wake가 가능하고, flag 부재(프로덕션 기본)에서는 현행 동작이 100% 보존되는 최소 candidate patch를 어떻게 안전하게 설계·검증하는가?

## A — Answer 구현 요약 (★ 옵션2 고정)
`dispatch/anu_pickup_driver.py` **1파일만** 수정:
1. import 추가: `functools`, `hmac`, `_default_sealed_key_loader`(runner, read-only), `launch_wake`(launcher, read-only).
2. 상수: `REAL_WAKE_FLAG_REL = "memory/state/p0b_real_wake_enabled"`, `REAL_WAKE_ENABLED = "enabled"`.
3. `read_real_wake_enabled(root, *, flag_reader=None)`: flag 첫 줄 trim == "enabled"일 때만 True. 부재/실패/그 외 값 → False(fail-closed).
4. `build_launcher_fn(root, *, real_wake_reader, sealed_key_loader, launch_wake_fn)`:
   - real-wake flag != enabled → `None`(sealed loader 호출조차 안 함).
   - flag on + sealed key 부재 → `None`(fail-closed, real wake 0).
   - flag on + sealed key 존재 → `functools.partial(launch_wake, dry_run=False, anu_key_verifier=verifier)`. verifier = `hmac.compare_digest`(상수시간 비교).
5. `main()`: `launcher_fn = build_launcher_fn(CANONICAL_ROOT)` 후 `scan_once(..., launcher_fn=launcher_fn)`.
   - ★ 프로덕션 기본 = real-wake flag 부재 → `build_launcher_fn`이 None 반환 → **현행 동작 100% 동일**(launch_wake 코드경로 미실행).

runner / launcher / env_loader / entrypoint / systemd service는 **무변경**(read-only).

## 생성/수정 파일 (EXPECTED 5)
1. `dispatch/anu_pickup_driver.py` — main() launcher_fn 결선 + real-wake flag 게이팅(수정)
2. `tests/regression/test_sealed_key_and_launcher_wiring_2729p13.py` — 회귀 테스트 26개(신규)
3. `memory/reports/task-2729+13.md` — 본 보고서(신규)
4. `memory/plans/p0b-pickup/sealed_key_and_launcher_wiring_design_260607.md` — 설계 문서(신규)
5. `memory/events/task-2729+13.l1-smoke-evidence.txt` — L1 isolated smoke 증거(신규)

## sealed 키 공급 옵션 비교 (A/B/C)
- **A) `.env.keys` `COKACDIR_KEY_ANU` (현 로더 기본)**: 노출면=파일 1개(권한 의존), 회전=파일 교체+재기동, rollback=즉시. 위험=평문 파일이나 현재 fail-closed 로더와 정합. (현행 채택)
- **B) systemd `EnvironmentFile=`(0600)**: 노출면=systemd 단위+env, 회전=파일 교체+`daemon-reload`, rollback=용이. 위험=프로세스 env 노출 가능. 키 provision은 ops 영역(본 task 범위 외).
- **C) 외부 secret store→env injection**: 노출면 최소(메모리 주입), 회전=store 정책, rollback=store 버전. 위험=의존성/가용성. 도입 비용 큼.
- ★ 실제 키 값 provision은 회장/ops 책임(본 task에서 0). 로더 인터페이스(`sealed_key_loader`)는 A/B/C 모두 동일하게 수용.

## 테스트 결과
- 신규 회귀: `test_sealed_key_and_launcher_wiring_2729p13.py` — **26 passed** (subprocess sabotage autouse fixture 하).
- 회귀 방지: driver(2721) 34 + runner(2720) 12 + real_wake_wiring(2729+8) 25 + activation_hardening(2729+7) 11 = 기존 82 passed. 총 108 passed, 회귀 0.

## L1 스모크테스트 결과 (isolated temp + mock provider)
- 서버 재시작: 해당없음(subprocess/decision 모듈 — 서버 아님).
- API 응답 확인: 해당없음. 대신 **real launcher 코드경로를 mock runner로 실행**(실제 subprocess 0).
- L1 테스트 결과(증거): 신규 회귀 **26 passed** (pytest, subprocess sabotage fixture 하), 관련 모듈 전체 **108 passed** — 실제 subprocess 0.
- 증거 파일: `memory/events/task-2729+13.l1-smoke-evidence.txt`
  - [L1-1] `build_launcher_fn(CANONICAL_ROOT)` [flag 부재] = **None** (프로덕션 default = ACTIVE false, canonical write 0)
  - [L1-2] flag on + sealed key 부재 → **None** (fail-closed)
  - [L1-3] flag on + mock sealed key → partial → mock runner로 **LAUNCHED** (실제 subprocess 0), `to_json()`에 raw key/argv 미노출(argv_len만)
  - [L1-4] 잘못된 key → **FAIL_CLOSED_NON_ANU_KEY** (wake 0), runner 추가 호출 0
  - subprocess.run/Popen/call/os.system **sabotage 활성 상태에서 전 항목 PASS** → 실제 spawn 0 보장.
- canonical `memory/p0b_state/driver_runs.jsonl` md5 BEFORE==AFTER → **canonical delta 0**.

## 필수 확인 10 (회장 verbatim)
1. CODE=`${HOME}/p0b-pickup-main`, DATA=canonical(`${HOME}/workspace`) — entrypoint.sh L8-10 확인. ✓
2. production entrypoint 실행 코드 경로 = CODE_ROOT (`cd CODE_ROOT; PYTHONPATH=CODE_ROOT python3 -m dispatch.anu_pickup_driver`). ✓
3. launcher_fn 결선 위치 = `main()`(`build_launcher_fn(CANONICAL_ROOT)` → `scan_once(launcher_fn=...)`). ✓
4. sealed key env = `COKACDIR_KEY_ANU`, 부재 시 fail-closed = `PICKUP_SEALED_KEY_MISSING`(wake 0). ✓
5. key 부재 시 real wake 미실행: `build_launcher_fn` → None / `pickup_once` → SEALED_KEY_MISSING. ✓
6. key 존재 시 argv 생성되나 raw key 출력 0(launcher argv_len만). ✓
7. logs/audit/ledger에 argv_len/redacted만(`LaunchRecord.to_json()`에 argv/key 부재). ✓
8. duplicate wake 방지(dedupe ledger) + ledger/marker fail-safe 유지(회귀 통과). ✓
9. activation_epoch 이전 legacy 140 영구 skip invariant 유지(VERDICT_NOOP_LEGACY_SKIP 회귀 통과). ✓
10. ACTIVE=false 유지: real_wake flag/driver flag/epoch 미생성, systemd not-enabled. ✓

## 필수 검증 15 — 전부 PASS (회귀 26 + L1 증거)
1.key present/absent ✓ 2.absent→wake 0 ✓ 3.present→argv·raw key 0 ✓ 4.flag off→launcher_fn=None·호출 0 ✓ 5.flag on→mock만·실제 subprocess 0 ✓ 6.legacy skip ✓ 7.duplicate 0 ✓ 8.terminal no-op ✓ 9.ledger/marker fail-safe ✓ 10.raw key 0 ✓ 11.ACTIVE=false ✓ 12.systemd not enabled ✓ 13.activation_epoch absent ✓ 14.canonical delta 0 ✓ 15.subprocess sabotage 하 PASS ✓

## 발견 이슈 및 해결
- **(해결) L1 하니스 초기판 실수**: 최초 L1-3에서 `build_launcher_fn`의 partial을 mock runner 없이 직접 호출하여 기본 runner(`subprocess.run`)가 **실제 cokacdir를 1회 spawn**(더미 키 `SEALEDKEY_DUMMY` + 무효 argv → cokacdir `status:error`로 거부, cron/wake/callback 등록 0). `--cron-list`로 부작용 0 확인. 하니스를 mock runner + subprocess sabotage로 교정 재실행하여 실제 spawn 0 달성. **패치 자체 결함 아님**(프로덕션은 flag 부재→launcher_fn=None로 이 경로 미실행).
- (해결) pre-commit start_task_guard: 브랜치를 `task/task-2729+13-dev2`로 정규화 + `.tasks/locks/task-2729+13.lock` 생성으로 통과.

## ★ 완료 상태 / 환경 블로커 (EXTERNAL_DIRTY_BLOCKER)
- 구현/검증은 **완료**(PR_READY_CANDIDATE 달성, CODE_ROOT 6커밋, 108 passed, QC PASS, scope PASS).
- 그러나 `finish-task.sh`의 `.done` 생성이 **환경 블로커로 차단**됨:
  - GIT-GATE가 canonical(`/home/jay/workspace`) 워크트리를 검사하는데, 내 task와 **무관한 기존 dirty 4건**이 존재:
    `utils/replacement_pr_runner.py`, `memory/specs/anu-system-spec.md`, `memory/specs/anu-system-spec-changelog.md`, `tests/regression/test_replacement_pr_runner_2510.py`
    (mtime 06-05 ~ 06-07 18:15 — **task 시작(21:14) 이전** 생성. task 책임 아님.)
  - 시스템 자동 분류: `EXTERNAL_DIRTY_BLOCKER` (`memory/events/task-2729+13.external-dirty-blocker.json`),
    callback-cause = `FINISH_TASK_GIT_GATE_BLOCKED / EXTERNAL_DIRTY_BLOCKER`,
    remediation = **"origin/main sync 또는 무관 dirty 정리 — task 재실행 불필요"**.
  - 본 task doctrine은 **canonical reset/clean/stash/switch 금지**이므로 봇이 dirty를 정리할 수 없음 → ANU/ops 영역.
- 멱등 마커(`.qc-done`, `.scope-guard-done`) 보존됨 → canonical dirty 정리 후 `finish-task.sh task-2729+13 dev2` 재실행 시 GIT-GATE만 통과하면 즉시 `.done` 생성됨(코드 재작업 불필요).
- ★ ANU normal callback: finish-task가 GIT-GATE에서 차단되어 notify-completion 미도달 → 콜백 미등록. canonical 정리 후 finish-task 재실행으로 ANU(독립 key <ANU_KEY_REDACTED>) 콜백 등록이 정상 수행됨.

## 머지 판단
- 머지 필요: **No (보류)** — 회장 지시: PR 생성·CI/Gemini gate·merge·activation은 **별도 승인 전까지 0**.
- 브랜치: `task/task-2729+13-dev2` (CODE_ROOT `/home/jay/p0b-pickup-main`)
- 머지 의견: 코드 정합·테스트 108 passed·canonical 무손상·ACTIVE=false 확인. PR_READY 상태이나 회장 승인 대기.

## 모델 사용 기록
- 토르(백엔드): sonnet — driver 패치 + 설계 문서 (Lv.3 로직 구현, haiku 부적합).
- 헤임달(테스터): sonnet — 보안 회귀 테스트 26개 (보안 민감, haiku 부적합).
- 오딘(팀장, Opus): 설계/검토/통합/L1 스모크/보고서.

## 비고
- doctrine 준수: ANU normal callback collector(독립 ANU key)는 finish-task.sh가 강제 등록(self-key 차단). 본 보고서 작성 ≠ 완료, finish-task.sh 실행이 완료 경로.
