# real_merge_hooks chair_authorization snapshot 교차검증 정책 수정 spec — 260523

회장 결정(2026-05-23 D안): real_merge_hooks 정책에 chair_authorization expected_files_snapshot 교차 검증 도입. forbidden prefix 정적 가드 보안 의미 유지 + snapshot 정합 시 한정 허용. **B안(우회) / C안(다른 target) 모두 기각** — pilot 가치 보존 + 근본 정책 충돌 해소.

기반: `system_real_merge_executor_wiring_spec_260523.md` §14(forbidden paths) + B1 pilot NO_OP_FORBIDDEN_PATH 사고 (`memory/events/real_merge/pr_141/ed77e768b1d930cbb9ed9da09b36d651c7184dac/merge_decision.json` · 14:21:36 KST).

---

## 1. 사고 분류
- **분류**: REAL_MERGE_FORBIDDEN_GUARD_VS_AUTHORIZATION_SNAPSHOT_MISMATCH
- **Critical7 해당**: ❌ 아님 (보안 가드 정상 fail-closed)
- **시스템 결함**: ❌ 정책 정의 부족 (가드 vs snapshot 우선순위 미명시)
- **chair_authorization 발급 후 처리**: ✅ EXPIRED_UNUSED_PER_CHAIR_DECISION_260523 marked

---

## 2. 목표 (회장 verbatim 10항목)

1. forbidden prefix 검사 **전에** chair_authorization expected_files_snapshot 로드
2. changed_files **전체** 가 expected_files_snapshot 의 **부분집합** 인지 확인
3. PR number / head_sha 가 chair_authorization 과 **정확 일치** 확인
4. snapshot 에 **없는** forbidden prefix 파일 → 계속 `NO_OP_FORBIDDEN_PATH`
5. snapshot 에 **있는** 파일이라도 production / secret / admin override / expected_files 밖 수정 시 차단
6. `.tasks/locks/*` 같은 sanctioned push-gate artifact 는 task 산출물과 **분리 기록**
7. `tests/fixtures/INDEX.md` 같은 fixture/doc-only 파일 → snapshot 일치 시 **통과 가능**
8. allow 이유를 `merge_decision.json` 에 명시 (예: `allow_reason="chair_authorization_snapshot_crossref"`)
9. **broad prefix allowlist 금지** (snapshot exact match 만 허용)
10. **기존 forbidden path 보안 의미 유지** (snapshot 정합 없으면 동일 동작)

---

## 3. 새 검증 흐름 (Step 0 정정)

```
Step 0a — 입력 검증
  changed_files None → ["__INPUT_NONE_FAIL_CLOSED__"] (기존 round-1 fix 유지)
  changed_files = [] → forbidden 0 (기존 동작 유지)

Step 0b — chair_authorization snapshot 교차 검증 (신규)
  if chair_authorization 부재 → 기존 검증 (NO_OP_NO_AUTHORIZATION 후속)
  if chair_authorization 존재:
    pr_match  = pr_identity.pr ∈ chair_authorization.pr_numbers
    sha_match = pr_identity.head_sha ∈ chair_authorization.head_shas
    if not (pr_match AND sha_match):
      → NO_OP_AUTH_MISMATCH (CHAIR_REQUIRED · 별도 신규 enum)

Step 0c — snapshot allowlist (forbidden 가드 우선보다 우선)
  snapshot = chair_authorization.expected_files_snapshot (필드 부재 → 빈 set)
  unauthorized_forbidden_hits = []
  for path in changed_files:
    forbidden_hit = (path in FORBIDDEN_PATHS) or any(path.startswith(p) for p in FORBIDDEN_DIR_PREFIXES)
    if forbidden_hit:
      if path not in snapshot:
        unauthorized_forbidden_hits.append(path)
      # else: snapshot exact match → allow (allow_reason 기록)
  if unauthorized_forbidden_hits:
    → NO_OP_FORBIDDEN_PATH (기존 doctrine 유지)

Step 0d — sanctioned artifact 분리
  sanctioned = [p for p in changed_files if p.startswith(".tasks/locks/")]
  task_outputs = [p for p in changed_files if p not in sanctioned]
  merge_decision 에 sanctioned + task_outputs 분리 기록

Step 0e — production / secret / admin override 재확인
  (gate_snapshot §4 검증 단계 그대로 유지 · 본 단계는 forbidden 우회 아님)
```

**핵심 doctrine**: forbidden prefix 보안 가드는 **유지** · chair_authorization snapshot 의 **exact match** 만 통과 (broad allowlist 금지). production 영역 (utils/, scripts/, dispatch/) 은 snapshot 에 있더라도 § 추가 검증 단계에서 CHAIR_REQUIRED 격상.

---

## 4. 신규 enum
```
NO_OP_AUTH_MISMATCH                 — chair_authorization 의 pr/head_sha 불일치 (Step 0b)
NO_OP_FORBIDDEN_PATH (기존)          — snapshot 외부 forbidden hit (Step 0c)
CHAIR_REQUIRED_PRODUCTION_IN_SNAPSHOT — snapshot 에 utils/*.py 또는 dispatch/* 포함 (Step 0e)
CHAIR_REQUIRED_BLOCKING_SECRET_IN_SNAPSHOT — snapshot 에 ghp_/PEM 등 (Step 0e)
CHAIR_REQUIRED_ADMIN_OVERRIDE_REQUIRED — admin override gate hit (기존)
```

---

## 5. merge_decision.json schema 확장

```json
{
  "schema": "real_merge.decision.v2",
  ...
  "result_enum": "NO_OP_FORBIDDEN_PATH" | "NO_OP_AUTH_MISMATCH" | ...,
  "allow_reason": "chair_authorization_snapshot_crossref" | null,
  "snapshot_crossref": {
    "snapshot_keys": [...],
    "changed_files_classification": {
      "task_outputs": [...],
      "sanctioned_artifacts": [...],
      "unauthorized_forbidden_hits": [...]
    },
    "pr_match": <bool>,
    "sha_match": <bool>
  },
  ...
}
```

---

## 6. 필수 구현 (task-2639 범위)

### 신규 helper
- `utils/snapshot_crossref_validator.py` — Step 0b/0c/0d 통합
  - `validate_snapshot_crossref(pr_identity, chair_authorization, changed_files) -> SnapshotCrossrefResult`
  - 결과: `{pr_match, sha_match, snapshot_keys, classification, unauthorized_forbidden_hits, allow_reason}`

### 정책 결선 수정 (최소 변경)
- `utils/real_merge_hooks.py` — Step 0 흐름 갱신
  - `_step0a_input_validation` (기존 유지)
  - `_step0b_authorization_match` (신규)
  - `_step0c_snapshot_crossref` (신규 · forbidden 가드 호출 위치 정정)
  - `_step0d_sanctioned_artifact_split` (신규)
  - `_step0e_existing_gates` (기존 유지)
  - schema bump v1 → v2 (`real_merge.decision.v2`)

### artifact schema 확장
- `utils/real_merge_artifact_schema.py` — v2 필드 추가 (`allow_reason` / `snapshot_crossref`)

### fixture 신규 7 시나리오 (회장 verbatim)
`tests/fixtures/snapshot_crossref/<scenario>/{evidence.json, expected.json, PROVENANCE.md}` (7×3 = 21 files):
- `fixture_in_snapshot_pass_candidate` — tests/fixtures/X + snapshot exact + pr/sha 일치 → PASS candidate (allow_reason 기록)
- `fixture_in_snapshot_mismatch_no_op` — tests/fixtures/X but snapshot 미포함 → NO_OP_FORBIDDEN_PATH
- `fixture_wrong_head_sha` — tests/fixtures/X + snapshot 일치 but head_sha 불일치 → NO_OP_AUTH_MISMATCH
- `sanctioned_lock_separated` — .tasks/locks/task-N.lock + snapshot 미포함 → forbidden 검증 통과(분리 기록) but task_outputs 와 분리
- `production_in_snapshot_chair_required` — utils/foo.py 가 snapshot 에 있음 → CHAIR_REQUIRED_PRODUCTION_IN_SNAPSHOT
- `blocking_secret_in_snapshot_chair_required` — ghp_/PEM 포함 fixture → CHAIR_REQUIRED_BLOCKING_SECRET_IN_SNAPSHOT
- `admin_override_required_chair_required` — admin_override_required gate=true → CHAIR_REQUIRED_ADMIN_OVERRIDE_REQUIRED

### regression 신규
- `tests/regression/test_snapshot_crossref_validator.py` — 7 fixture parametrized
- `tests/regression/test_real_merge_hooks_v2_step0_flow.py` — Step 0 흐름 + schema v2 + allow_reason 단언
- 기존 regression 8 + canonical-root 63 + callback consistency 40 + task-2635 helper 68 + baseline 264 = **476 유지**

### 안전 불변식 (회장 verbatim)
- forbidden prefix 보안 의미 **유지** (snapshot 정합 없으면 동일 fail-closed)
- broad prefix allowlist **금지** (snapshot exact match 만)
- ANU key `c119085addb0f8b7` 단일 출처 유지 (변경 0)
- envelope UTF-8 ≤3900 bytes 유지
- live cokacdir/subprocess 실호출 0 (regression mock)
- replacement_pr_runner / finish-task.sh / merge_ready_classifier / merge_ready_dryrun_executor **무수정**
- callback_envelope_schema / anu_callback_registrar / canonical_root_resolver / anu_collector_action_trigger / activation_flag_validator / chair_authorization_validator / gate_snapshot_validator / dispatch/finalize_hooks / dispatch/__init__ 무수정 (real_merge_hooks 만 정정)
- expected_files 외부 수정 0
- BLOCKING_SECRET 0

---

## 7. expected_files (task-2639 범위)

신규:
- `utils/snapshot_crossref_validator.py`
- `tests/fixtures/snapshot_crossref/<7 시나리오>/{evidence.json,expected.json,PROVENANCE.md}` (21 files)
- (선택) `tests/fixtures/snapshot_crossref/INDEX.md`
- `tests/regression/test_snapshot_crossref_validator.py`
- `tests/regression/test_real_merge_hooks_v2_step0_flow.py`

수정 (최소 결선):
- `utils/real_merge_hooks.py` — Step 0 흐름 정정 + schema v2 bump
- `utils/real_merge_artifact_schema.py` — v2 필드 추가

총 ~28 files. **프로덕션 영향**: real_merge_hooks Step 0 정정 + artifact schema v2 확장. 나머지 wiring stack 무수정.

---

## 8. 자동수렴
- Gemini medium/style/quality + expected_files 내부 → 자동수렴
- expected_files 내부 non-critical HIGH 자동수렴, 동일 함수 HIGH 반복 시 LOOP_BOUNDARY → 회장 보고
- 회장 보고 트리거: Critical7 / credential expansion / expected_files 밖 / admin override / replacement_pr fail / smoke fail

---

## 9. 금지 (회장 verbatim)

- PR #141 직접 merge 금지 (정책 수정 머지 후 재검증)
- 기존 chair_authorization 재사용 금지 (EXPIRED_UNUSED 처리됨)
- real auto-merge batch activation 금지
- admin override 금지
- branch protection 우회 금지
- foreign dirty 정리 금지
- replacement_pr_runner 수정 금지
- finish-task.sh 수정 금지
- NL intake 코드 구현 금지
- broad prefix allowlist 도입 금지

---

## 10. finalize 프로토콜 (★ BOT App token 부재 — 로컬 한정)
1. base = 최신 origin/main fa72e25a (PR #140 real merge wiring OFF-mode 머지분) clean worktree
2. 신규 helper 1 + fixture 21 + regression 2 + real_merge_hooks/artifact_schema 정정 전부 PASS + full new-fail 0 + 기존 476 유지
3. **로컬 commit만** (push/PR/merge 금지)
4. ANU normal completion callback — registrar 패턴 그대로 + envelope 5축 + canonical_root 명시
5. callback envelope UTF-8 ≤3900 bytes
6. executor 시작/종료 ts·로컬 commit SHA 명기

이후 ANU: 봇 로컬 commit fresh main 재적층 → OWNER push → PR open → Gemini 자동수렴 → 회장 보고 → 회장 merge 승인.

정책 수정 merge 후 → 새 chair_authorization 발급 (새 PR/head_sha 기준) → B1 pilot 재시도 또는 새 pilot target 결정.

---

## 11. frozen anchor
- ANCHOR-1: "Step 0 흐름: input validation → authorization match → snapshot crossref → sanctioned artifact split → existing gates"
- ANCHOR-2: "forbidden prefix 보안 가드 의미 유지 · snapshot exact match 만 통과 · broad allowlist 금지"
- ANCHOR-3: "schema v1 → v2 bump · `allow_reason` + `snapshot_crossref` 필드 추가 · merge_decision.json 분류 기록"
- ANCHOR-4: "production / secret / admin override 검출 시 CHAIR_REQUIRED 격상 (snapshot 포함 무관)"
- ANCHOR-5: "sanctioned (.tasks/locks/) artifact 분리 기록 · task_outputs 와 컬럼 분리"
- ANCHOR-6: "real_merge_hooks 만 정정 · 나머지 wiring stack (callback_envelope_schema/registrar/canonical_root_resolver/finalize_hooks 등) 무수정"
- ANCHOR-7: "PR #141 직접 merge 금지 · 기존 chair_authorization EXPIRED_UNUSED · 새 발급은 정책 수정 머지 후"
