# task-2729+4 — ANU Pickup 활성화 Preflight + Controlled Pilot 설계 보고서

> 작성: 비슈누(개발4팀장) | 팀: dev4-team | 일시: 2026-06-05 20:30 KST
> 성격: **read-only preflight + pilot 설계 (실행 0)** | 회장 인가 범위: 활성화 전 안전성 검증 + 1회성 pilot 설계까지만
> canonical 후보 worktree: `/home/jay/workspace/.worktrees/task-2729+2-dev2` (메인 트리 미존재 = ACTIVE=false 확증)

---

## SCQA 요약

**S (Situation)**: ANU result-pickup 자동화(봇 result.json → systemd `anu-pickup.path` 감지 → `anu-pickup.service` → `anu_pickup_entrypoint.sh` → `anu_pickup_driver.py` → `anu_result_pickup_runner.py`)는 5개 파일·1084줄로 IMPLEMENTED + WIRED 상태이며, 후보 worktree에만 존재하고 메인 트리·systemd user 영역에는 미설치(ACTIVE=false)다.

**C (Complication)**: 봇 종료 후 ANU 자동 wake가 0건이라 callback 회수가 수동에 의존한다. 그러나 production ACTIVE 즉시 전환은 중복 wake·runaway spawn·ANU key 노출 위험을 동반하여, 활성화 전 안전성 검증 없이는 전환할 수 없다.

**Q (Question)**: 8항목 read-only preflight 기준으로, 실제 service 실행·ANU spawn 0의 조건에서 이 구현이 활성화 가능한 안전 수준인가?

**A (Answer)**: preflight 8항목 중 **7항목 PASS, 1항목(ANU key raw 스캔) CAVEAT(스캐너 false positive)** 로, 결선·idempotency·terminal-marker noop·중복방지·key sealing·runaway 가드가 코드·테스트(46 passed in 0.92s)로 확인됐다. 단 `scan_once` MAX_FILES 상한 부재 등 CAVEAT 3건을 활성화 전 보완 권고하며, **ACTIVE=false 유지**하에 mock fixture 1건으로 1회성 pilot을 설계했다. systemctl enable --now·production ACTIVE는 회장 별도 승인 사항으로 본 task 범위 외다.

---

## Part A — Activation Preflight 8항목 결과 (read-only 증거)

### Item 1 — 결선(wiring) 확인 — ✅ PASS
5개 파일의 호출 체인이 코드상 완결됨:
- `anu-pickup.path:5` `PathExistsGlob=%h/workspace/memory/events/task-*.result.json` → `:6` `Unit=anu-pickup.service`
- `anu-pickup.service:5` `Type=oneshot` → `:8` `ExecStart=/bin/bash %h/workspace/scripts/anu_pickup_entrypoint.sh`
- `entrypoint.sh:49` `PYTHONPATH="${WORKSPACE}" python3 -m dispatch.anu_pickup_driver` (인자 0)
- `driver.py:21` `from dispatch.anu_result_pickup_runner import pickup_once` → `:307` `pickup_fn = pickup_fn or pickup_once` → `:459` `res = pickup_fn(path, executor_key=..., ledger_path=...)`
- `runner.py:34/284` 외부 모듈 `anu_runner_pickup_and_fire` 재사용(발사 로직은 `dispatch.anu_owned_callback_enforcement` 소관 — 분석범위 외)

### Item 2 — systemd user unit 상태 — ✅ PASS (미설치·inactive 확증)
```
systemctl --user is-enabled anu-pickup.path     → not-found
systemctl --user is-active  anu-pickup.path     → inactive
systemctl --user is-enabled anu-pickup.service  → not-found
systemctl --user is-active  anu-pickup.service  → inactive
list-unit-files | grep anu-pickup               → NOT_INSTALLED (0건)
~/.config/systemd/user/anu-pickup*              → NO_USER_UNIT_FILES
```
현재 자동 wake 발생 경로 **0** — 회장 금지사항(즉시 ACTIVE 전환) 위반 위험 없음.

### Item 3 — 감지경로 + debounce/lock/idempotency — ✅ PASS (CAVEAT 1)
- 감지경로 일치: path unit `PathExistsGlob` = driver glob(`driver.py:548` `RESULT_GLOB`) = `memory/events/task-*.result.json`
- debounce(readiness): `driver.py:40-41` `STABLE_SEC=2.0`, 3회 size/mtime 안정성 재샘플 → 최근 작성 파일은 `NOOP_NOT_READY`(DEFER)
- lock: `entrypoint.sh:41-45` `exec 9>"${LOCK}"; flock -n 9 || exit 0` (+ `:38-40` symlink LOCK 차단)
- idempotency: runner 3·4단계(done/acked 존재 → `SKIP_TERMINAL`, dedupe ledger → `SKIP_DEDUPE`) + driver `:441-455` 사전 dedupe
- **CAVEAT**: systemd `PathExistsGlob`는 잔존 파일에 대해 부팅 시 즉시 트리거 → done marker+dedupe ledger 이중 guard로 실 중복처리는 차단되나 불필요 oneshot 1회 가능

### Item 4 — terminal marker(.done/.acked) noop — ✅ PASS
- runner: `runner.py:186-190` done/acked 존재 시 `PICKUP_SKIP_TERMINAL` 반환(wake_built=False)
- driver: `driver.py:439-455` 동일 조건 + `_dedupe_hit()` → `VERDICT_PICKUP_SKIP` (pickup_fn 미호출)
- 2-레이어(runner+driver) 이중 차단으로 no-op 보장

### Item 5 — 동일 result.json 중복 처리 방지 — ✅ PASS
- 처리완료 마커 원자작성: `runner.py:315-336` `os.replace(tmp, done_path)` (+ fsync)
- dedupe ledger append: `runner.py:299-313` `(task_id, sha256, PICKUP_WAKE_BUILT)` → `callback_4tuple_index.jsonl`
- 재실행 스킵: ledger 일치 → `SKIP_DEDUPE` / done 존재 → `SKIP_TERMINAL`
- processed 이동: `driver.py:473-481` `_move_processed()` → 감시 디렉토리 밖으로 원자 이동(재감지 차단)

### Item 6 — ANU key raw 노출 0 — ✅ PASS (raw 노출 없음)
- `runner.py:58` 환경변수명 상수만(`COKACDIR_KEY_ANU`), `:62-71` `.env.keys`→env로만 sealed 로드
- key는 변수/ kwargs 전달만(`:265`, `:280`); `to_json()`·`DriverRecord`(`driver.py:201` 주석 "ANU key literal 절대 미기록") 출력 스키마에 key 필드 없음
- `entrypoint.sh:5` 주석 "argv 에 키 literal 0", `:49` 인자 없는 모듈 호출 — argv/print/log/echo에 key literal 나타나는 라인 0
- (본 보고서에 실제 key 값 미기재 — 마스킹 준수)

### Item 7 — runaway 방지 — ✅ PASS (CAVEAT 1)
- `Type=oneshot` + `Restart=` 부재 → 자동 재시작 0
- `flock -n` non-blocking → lock 실패 즉시 exit(queue 누적 방지)
- activation flag 이중 가드: `entrypoint.sh:14-21`(값≠"enabled" 시 exit 0) + `driver.py:535` `is_activated` False → `NOOP_DISABLED`
- processed 이동으로 동일 파일 재트리거 0
- **CAVEAT**: `scan_once`(`driver.py:546-573`)에 `MAX_FILES`(건당 처리 상한) 부재 → events에 result.json 대량 축적 시 단일 oneshot 장시간 점유 가능

### Item 8 — dry-run / mock decision-only (실 service·ANU spawn 0) — ✅ PASS
- 실 spawn 0 코드 확증: runner/driver 모두 `import subprocess`·`subprocess.` 호출 **0건**; WAKE_BUILT는 argv dict 반환만(`driver.py:472` 주석 "driver 는 argv 를 실행하지 않음")
- decision 경로(정적): ① `scan_once` activation False → `NOOP_DISABLED` ② `process_one` 6조건 미충족 → QUARANTINE/NOOP ③ `pickup_once` sealed_key 미설정 → `PICKUP_SEALED_KEY_MISSING` (spawn 도달 불가)
- mock 검증법: `scan_once(root, pickup_fn=mock_fn, flag_reader=lambda:"enabled")` 주입 — 실 ANU spawn 없이 decision 확인 가능
- **preflight 스크립트**(`scripts/harness/v36/anu_pickup_preflight_check.py`) 실행: overall=CAVEAT (systemd_units/candidate_files/safety_keywords/dry_run_guard=PASS, raw_key_exposure=CAVEAT)

### 회귀테스트 (mock 기반, 네트워크·subprocess 0 확인 후 실행)
```
test_anu_result_pickup_runner_2720.py (9) + test_anu_pickup_driver_2721.py (20+param)
→ 46 passed in 0.92s
```
커버 속성: idempotency·terminal-marker noop·dedup·key sealing(literal 0건)·path traversal quarantine·write-race DEFER·runaway 소멸(WAKE_BUILT 후 watched 0건)·fail-safe(invalid UTF-8/shutil.Error)

---

## Part B — Controlled Pilot 설계 (실행 아님 — 설계만)

### 대상 (둘 중 1개로 제한)
- (a) 신규 테스트 result.json 1개 (`task-pilot-2729p4.result.json`, /tmp 또는 격리 fixture) — 또는
- (b) terminal marker(`*.pickup.done`) 선존재 noop fixture 1개

### 실행 방식 (실 service 미기동)
- systemd 미사용. `scan_once(pilot_root, pickup_fn=mock_fn, flag_reader=lambda:"enabled")` 직접 호출로 decision만 관측
- pilot_root = 격리 임시 디렉토리(메인 events 미오염). pilot 후 fixture 삭제

### 성공 기준 (6)
1. pickup runner(mock_fn) **정확히 1회**만 발사 — 2. ANU key raw **0** — 3. duplicate pickup **0**(2회차 SKIP_DEDUPE/TERMINAL) — 4. terminal marker 존재 fixture → **no-op** — 5. 실 PR merge/push/comment **0** — 6. result 상태 정확히 ANU 보고(WAKE_BUILT/SKIP verdict)

### 사후 상태
- pilot 후에도 **ACTIVE=false 유지**. systemctl enable --now·production ACTIVE = **회장 별도 승인**.

---

## 활성화 시 위험 / 완화 (활성화 전 보완 권고)

1. **[CAVEAT-1] `scan_once` MAX_FILES 상한 부재**(driver.py:546-573) — 완화: 건당 처리 상한(예: 1회 N개) + 초과분 다음 트리거 이월. 활성화 전 적용 권고.
2. **[CAVEAT-2] dedupe ledger append 실패 비치명 처리**(runner.py:312-313) — 완화: ledger 실패 시 done marker 단독 의존이나, 둘 다 실패 시 중복 wake 가능 → 실패 카운터/알림 추가 권고.
3. **[CAVEAT-3] done marker 작성 실패 시 WAKE_BUILT 반환 계속**(runner.py:331-336) — 완화: marker 실패를 비-WAKE로 강등하거나 재시도. ledger 이중화로 잔여 위험 수용 가능.
4. **[LOW] PathExistsGlob 부팅 즉시 트리거** — 완화: 부팅 후 processed 미이동 잔존 파일 정리 hook. 이중 guard로 실 중복은 차단.
5. **[CAVEAT-스캐너] raw_key_exposure CAVEAT** — 286 검출 중 대부분 `scripts/.codegraph-venv/` 서드파티 false positive. 후보 5개 파일 내 실 노출 0. 별도 수동확인 권고 4건은 본 task 범위 외 모듈.

---

## 활성화 절차안 (회장 승인 후 실행용 — 본 task에서 미실행)

1. 후보 PR 머지(메인 트리에 5개 파일 반영) — 현재 worktree-only
2. CAVEAT-1~3 보완 패치 적용 + 회귀테스트 재실행
3. activation flag 파일(`memory/state/p0b_driver_enabled`) = `enabled` 설정
4. systemd user unit 설치: `~/.config/systemd/user/`에 path/service 배치 → `systemctl --user daemon-reload`
5. **`systemctl --user enable --now anu-pickup.path`** (★ 회장 별도 승인 필수 — 본 task 금지사항)
6. 1회성 pilot(Part B) → 6 성공기준 충족 확인 → production 모니터링 전환

---

## L1 스모크테스트 결과
- **서버 재시작**: 해당없음 (read-only preflight, 서버/프론트 무관)
- **API 응답 확인**: 해당없음 (API 무관). 대신 preflight 스크립트 실 실행: `python3 scripts/harness/v36/anu_pickup_preflight_check.py` → overall=CAVEAT(4 PASS/1 CAVEAT) 정상 출력 + 회귀테스트 46 passed
- **systemctl read-only 조회**: is-enabled=not-found / is-active=inactive (실 실행)
- **스크린샷**: 해당없음 (CLI/백엔드 검증)
- L1 통과: preflight 스크립트 실행 PASS + 회귀테스트 46 passed = 실동작 확인 완료

---

## 발견 이슈 및 해결

### 자체 해결 (1건)
1. **preflight 스크립트 pyright 타입 경고 3건** — `found_kw` None 연산(`:172`) → `(found_kw or '')` 가드, 미사용 import(`sys`, `Optional`) 제거. py_compile OK + 재실행 PASS 확인.

### 범위 외 미해결 (3건 — 활성화 전 보완 권고로 이관)
1. **CAVEAT-1 MAX_FILES 상한** — 범위 외 사유: 본 task는 read-only preflight, 구현 수정은 forbidden_paths(driver.py). 활성화 절차안 step 2로 이관.
2. **CAVEAT-2/3 ledger/marker 실패 처리** — 동일 사유(runner.py forbidden). 활성화 전 보완 권고.
3. **raw_key_exposure 수동확인 4건** — 범위 외 모듈(dispatch_callback_contract.py 등). ANU/로키 별도 점검 권고.

---

## 산출물
1. `memory/reports/task-2729+4-pickup-preflight.md` (본 보고서) + `memory/reports/task-2729+4.md` (동일)
2. `scripts/harness/v36/anu_pickup_preflight_check.py` (read-only·decision-only 점검 스크립트, py_compile OK, 실행 PASS)
3. `memory/events/task-2729+4.done` (finish-task.sh 생성)

## 모델 사용 기록
- 비슈누(팀장): Opus 4.8 — 설계/분배/통합/검토 + 스크립트 타입경고 trivial fix(2줄)
- 카르티케야(백엔드): Sonnet — preflight item 1/3/4/5/6/7 코드분석 (read-only)
- 하누만(테스터): Sonnet — item 2/8 systemd·dry/mock 검증 + preflight 스크립트 작성 + 회귀테스트
- haiku 미사용 (분석·검증 작업은 sonnet 이상 규칙 준수)

## 금지사항 준수 확인
production ACTIVE 전환 0 · systemctl enable --now 0 · ANU key raw 출력 0 · result.json 중복처리 유발 0 · merge/push/PR comment 자동실행 0 · 실 ANU spawn 0(전부 dry/mock·정적분석) · forbidden_paths(driver/runner/entrypoint/systemd unit) 수정 0
