# task-2774+6 — PR#252 remediation round1 (G3 fail-open 방지, guard-native)

상태명: `TASK2774_PR252_REMEDIATION_ROUND1_G3_FAILOPEN_FIX_GUARD_NATIVE_ACTIVE_FALSE`
작성: 오딘(개발2팀장) | 작업일: 2026-06-25

---

## S (Situation)
PR #252(head 8159dba5)는 activation-intended 전용 안전 게이트(`dispatch/anu_activation_preflight.py`)를 도입하는 guard-native PR이다(ACTIVE=false 유지). Gemini 리뷰에서 MED 3건이 지적되었고, 그중 2·3번은 **G3 backlog 보호 게이트의 fail-open 가능성**으로, 안전 게이트가 조용히 실패(미탐)하면 C 선행 PR 목적과 충돌한다.

## C (Complication)
- **MED-1**: activation flag 파일을 무제한 `readline()` 으로 읽어 손상·거대 flag 파일 시 OOM/비정상 진행 위험.
- **MED-2 (fail-open)**: `glob.glob` 패턴 기반 backlog 탐지 → 파일명/경로에 glob metacharacter(`[ ] ? *`)가 있으면 패턴 오인으로 **backlog 미탐(fail-open)**.
- **MED-3 (fail-open)**: canary paths 상대경로를 CWD 기준으로 해석 → root와 다른 CWD에서 실행 시 경로 오해석으로 **backlog 매칭 실패(fail-open)**.

## Q (Question)
expected_files 2개(`anu_activation_preflight.py`, 테스트) 안에서, driver 무접촉 원칙을 지키며 3건을 모두 **fail-closed**로 전환하고 의미(기존 STOP 동작·backlog 불변)를 보존할 수 있는가?

## A (Answer)
정확히 2파일만 수정하여 3건을 fail-closed로 닫고, 기존 11종 + 신규 5종 + 핵심 회귀 66종 전부 통과. driver 무접촉. merge는 final gate 전까지 보류.

---

## 수정 내역 (Gemini MED 3건 대응)

### MED-1 — flag read bounded (`_default_is_activated`)
- `fh.readline().strip()` → `fh.readline(100).strip()` (최대 100바이트만 읽음).
- 효과: 손상·거대 flag 파일(L1 실증 500KB)에서도 OOM/비정상 진행 0, 첫 줄 `enabled` 정상 판정.

### MED-2 — backlog 탐지 glob 제거 → listdir fail-closed
- `import glob` 및 `glob.glob(...)` 완전 제거(잔존 0건).
- `os.listdir(inbox_path)` 로 나열 후 `name.startswith("task-") and name.endswith(".result.json")` 필터.
- glob metacharacter(`[ ] ? *`) 포함 파일명도 패턴 오인 없이 정확 감지(미탐 0).
- fail-closed 정책:
  - `FileNotFoundError`/`NotADirectoryError`(inbox 미존재) → backlog 없음(정상, 기존 glob 빈 리스트와 동일).
  - 그 외 `OSError`(권한 등 나열 불가) → `INBOX_SCAN_ERROR` STOP (조용한 통과 금지).
- 읽기 나열(listdir)만 수행 — 파일 open/삭제/이동/소비 0.

### MED-3 — canary 상대경로 root 기준 정규화 fail-closed
- 신규 헬퍼 `_norm_under_root(root_real, p)`: 상대경로는 root 기준 join 후 `realpath`, root 밖/해소 불가 → `None`.
- `root_real = realpath(abspath(root))` 로 root 정규화 후 canary/backlog 모두 root 기준 비교.
- canary path가 1건이라도 `None`(root 밖/이상 경로/비교 불가) → `CANARY_PATH_INVALID` STOP (fail-closed).
- backlog 정규화 결과가 `None` 이거나 canary scope 밖 → `CANONICAL_BACKLOG_DETECTED` STOP.

### 의미 보존(불변) 확인
- backlog 1건이라도 canary 밖 → `CANONICAL_BACKLOG_DETECTED` STOP ✓
- paths None/빈목록 → `CANARY_PATHS_REQUIRED` STOP ✓
- backlog 삭제/수정/이동/소비 0(listdir만) ✓ (L1에서 before==after 실증)
- G2 5개 STOP reason 순서·동작 유지 ✓ (기존 테스트 4~6,10 PASS)
- 신규 reason 2개 추가 + `__all__` 등록: `INBOX_SCAN_ERROR`, `CANARY_PATH_INVALID`

---

## 수정 파일 (정확히 2파일 = expected_files 내, driver 무접촉)
- `dispatch/anu_activation_preflight.py` — MED 3건 수정 (커밋 `0d9a0c4d`, 토르)
- `tests/regression/test_activation_intended_safety_2774p5.py` — 신규 테스트 5종 추가 (커밋 `a41c58e8`, 헤임달)

`git diff 8159dba5..HEAD --name-only` = 위 2파일만. driver(`anu_pickup_driver.py`)/pickup_once/replacement_pr_runner 무접촉 확인.

## 신규 테스트 5종 (기존 11종 유지)
1. `test_default_is_activated_bounded_read_huge_flag` (MED-1: 200KB flag bounded read)
2. `test_preflight_backlog_with_glob_metacharacter_detected` (MED-2: `task-[abc].result.json` 등 정확 감지)
3. `test_preflight_relative_canary_resolved_under_root` (MED-3: 상대경로 root 기준 정규화 → READY)
4. `test_preflight_canary_outside_root_stop_failclosed` (MED-3: root 밖 → `CANARY_PATH_INVALID`)
5. `test_preflight_single_canary_no_backlog_consume` (MED-2: backlog 2개 개수·내용 불변)

---

## 테스트 결과
- 안전 회귀: `test_activation_intended_safety_2774p5.py` → **16 passed** (기존 11 + 신규 5)
- 핵심 회귀: driver_governor_wiring_2774p3 + anu_pickup_driver_2721 + anu_result_pickup_runner_2720 + spawn_safety_governor_2774 → **66 passed**
- `py_compile` (2파일) → OK

## L1 스모크테스트 결과 (실제 파일시스템 직접 실행 — pytest와 별개 실증)
- 서버 재시작: 해당없음 (라이브러리 게이트 모듈, 서버/API 아님)
- API 응답 확인: 해당없음
- subprocess/직접 실행 실증:
  - [MED-1] 거대 flag(500KB) bounded read → `activated=True` (OOM 0)
  - [MED-2] glob metachar 파일명 backlog → `CANONICAL_BACKLOG_DETECTED`, backlog before==after(불변)
  - [MED-3a] 상대경로 root 기준 정규화 → `ACTIVATION_INTENDED_READY` (CWD 기준이었다면 미탐 발생했을 케이스)
  - [MED-3b] root 밖 canary → `CANARY_PATH_INVALID` (fail-closed STOP)
- 스크린샷: 해당없음 (백엔드 게이트 모듈)

---

## 발견 이슈 및 해결
- pyright `Import "dispatch" could not be resolved` 경고: 테스트 파일의 동적 모듈 로딩 패턴에 대한 정적 분석 한계로, 기존 11종 테스트에도 동일하게 존재하던 사전 경고. 런타임 import는 정상(16 passed)이며 본 작업 비유발 → 비차단.

## 게이트
- **G1 설계**: affected_files 2개, 다른 팀 겹침 없음, driver 무접촉 ✓
- **G2 구현**: 팀 테스터(헤임달) 기능 테스트 16종 + 핵심 회귀 66종 PASS ✓
- **G3 머지**: PR #252에 push 완료. Gemini MED는 코드 fix push로 해소(임의 resolve 금지). **merge는 final gate(독립검증/CI/Gemini/final) 전까지 보류** — 본 작업 범위에서 merge 0.

## CI / Gemini 상태
- push 후 PR #252 head = `a41c58e8`, state=OPEN.
- CI/Gemini polling 루프는 미수행(작업 지시: ANU 독립검증). 후속 Gemini 재리뷰는 push된 fix로 MED thread 자동 갱신.

## 머지 판단
- **머지 필요**: No (final gate 전 merge 금지 — 작업 지시 명시)
- **브랜치**: task/task-2774+5-dev2
- **워크트리 경로**: /home/jay/workspace/.worktrees/task-2774+5-dev2
- **머지 의견**: 테스트 전부 PASS·2파일 한정·driver 무접촉·fail-closed 전환 완료. 단 독립검증/Gemini 재리뷰 unresolved 0 확인 후 final gate에서 머지. 현 단계 merge-ready 아님(보류).

## 비고
- ACTIVE=false 유지 (flag/systemd/canary/real fire 0, matrix true 0).
- remediation commit: `0d9a0c4d`(토르, 구현), `a41c58e8`(헤임달, 테스트).
- callback_schedule_created: false (OS-level pickup runner가 owner-proof 후 closeout 소유 — executor self-key 자가발사 0).

## ⚠ 완료 차단: finish-task.sh scope-guard 오탐 (아누 판단 필요)

finish-task.sh 실행 시 scope-guard가 **5건 위반**으로 머지/완료(.done)를 차단했습니다. 분석 결과 **stacked-PR remediation base 오탐(false-positive)** 으로 확정합니다.

### 차단된 위반 5건 (모두 이전 phase 산출물 — 본 task 무접촉)
- `dispatch/anu_pickup_driver.py` (forbidden_paths) — task-2774+5 phase 추가
- `dispatch/anu_result_pickup_runner.py` (forbidden_paths) — 이전 phase
- `tests/regression/test_driver_governor_wiring_2774p3.py` (scope 외) — 이전 phase
- `tests/regression/test_spawn_safety_governor_2774.py` (scope 외) — 이전 phase
- `utils/spawn_safety_governor.py` (scope 외) — 이전 phase

### 근본 원인 (직접 검증)
- 본 task의 remediation 실제 scope = `git diff 8159dba5..HEAD` = **정확히 2파일**(`anu_activation_preflight.py`, `test_activation_intended_safety_2774p5.py`). per-commit 확인: 0d9a0c4d=preflight 1파일, a41c58e8=test 1파일.
- 위반 5개 파일은 **8159dba5 시점에 이미 존재**(`git cat-file -e 8159dba5:<file>` 전부 EXISTS)하며, `git diff 8159dba5..HEAD -- <5개파일>` = **변경 0건**(무접촉 확정).
- finish-task.sh scope-guard의 base 산출:
  - non-strict(현재, `FINISH_TASK_WORKTREE_STRICT` unset=0): `git diff main..HEAD`(local main 177792e5 stale) → **7파일**(실제 scope-diff.txt와 일치 = 이 경로 실행됨).
  - strict: `merge-base(origin/main,HEAD)=23f1b6db..HEAD` → **3파일**(여기에도 forbidden `anu_pickup_driver.py` 포함 — 이전 phase가 추가했기 때문).
- → **어떤 finish-task.sh 모드도 remediation base(8159dba5)를 잡지 못함.** forbidden 파일 `anu_pickup_driver.py`는 PR#252 이전 phase(task-2774+5)의 정당한 산출물이라, PR base 기준 diff에는 항상 포함됨.
- `worktree-base.json` fallback은 strict 모드에서 merge-base가 **빈 경우에만** 사용되는데(line 770 `[ -z "$SCOPE_BASE" ]`), merge-base가 23f1b6db로 비어있지 않아 무효. non-strict 경로는 fallback을 아예 참조하지 않음.

### 조치 (우회 금지 doctrine 준수)
- finish-task.sh / task-scope-guard.sh **수정 금지**(forbidden_paths + 가드 우회 금지).
- `.done` / `.scope-guard-done` **수동 생성 금지**(우회 doctrine 위반).
- 시스템이 생성한 마커 유지: `task-2774+6.escalate`, `task-2774+6.failure-envelope.json`.

### 아누(개발실장) 판단 요청
1. 본 remediation의 진짜 scope = `8159dba5..HEAD` = 2파일(허용 paths 내), driver 무접촉을 독립 검증.
2. 검증 후 완료 처리(아누 권한으로 closeout) 또는,
3. (별도 인프라 task) finish-task.sh scope-base가 stacked-PR에서 `worktree-base.json` base_sha를 merge-base보다 우선 적용하도록 보강.

★ 본 task의 실제 deliverable(PR#252 push)은 **완료**. merge는 task 지시상 어차피 금지(final gate 전). 차단된 것은 finish-task.sh의 .done closeout 뿐.

## 모델 사용 기록
- 토르(백엔드, preflight.py 구현): sonnet
- 헤임달(테스터, 신규 테스트 5종): sonnet
- 통합 검증/push/보고(오딘): opus (팀장 직접 — 회귀/diff/L1 실증/push)
- haiku 미사용 (안전 게이트 코드·테스트로 정확성 우선).
