# task-2616 — ANU v3 CLI Output-Path Guard: 전수 read-only scan + 실행기반 remediation 설계안

> **SCAN/DESIGN ONLY.** 실제 코드수정·guard 구현·remediation 적용·추가 dispatch = 회장 별도 승인 전까지 0.
> 본 md 는 단독 완료 산출물이 아니다 — 정본 = `scripts/scan_anu_v3_cli_output_sinks_2616.py` + `memory/events/task-2616.scan-result.json` + `memory/fixtures/task-2616.detected-pattern.json` + `memory/events/task-2616.{regression-candidates,remediation-scope-proposal,decision,result}.json`.
> spec sha256 = `97aff727b662f05fc25e47528b2beaac08651e89fe599dd9a25b3a8231dd24e3` 확인 일치.

## 1. scan 요약 (read-only AST, scan 대상 byte-0)

- ANU v3 .py 전수: **63개**
- CLI entrypoint(argparse + __main__/main/_main): **6개**
- Critical7 후보(CLI 출력 인자 → 무검증 write_text): **3개**
- stdout-only 안전: **3개**
- arg-무관 file write(검토): **0개**

scan 대상 6개 파일 mtime/size 불변 확인 — scanner 는 `'r'` read + `ast.parse` 만 사용, write 모드로 대상 파일을 열지 않음.

## 2. 검출 모듈 / 라인 / 인자 / sink

### 2.1 Critical7 후보 (3건 — 동일 클래스: CLI 출력 인자 무검증 fs write)

1. **anu_v3/batch_hold_adjudicator.py** — `task-2610`
   - 인자: `--output` (line 642, `def _main` line 636)
   - sink: line 656 `Path(a.output).write_text(out + "\n", encoding="utf-8")`
   - 안전 default: line 658 `sys.stdout.write(out + "\n")` (미지정 시)
   - 상태: **UNREMEDIATED.** task-2610 Track A durable success 는 adjudication 로직 한정 — `--output` sink 미교정.

2. **anu_v3/batch_dependency_classifier.py** — `task-2613` / `task-2613+1`
   - 인자: `--out` (line 695, `def main` line 679)
   - sink: line 725 `Path(args.out).write_text(text, encoding="utf-8")`
   - 안전 default: line 728 `print(text)`
   - 상태: **ESTABLISHED Critical7** (`memory/events/task-2613+1.critical7-proof-packet_260519.json`). CHAIR_HOLD·auto-remediation 0. task-2613+1 remediation 은 dependency 로직(non-Critical HIGH×1+MEDIUM×1) 한정 — `--out` Critical7 의도적 보류.

3. **anu_v3/pre_authorized_evidence_bundle_builder.py** — task 매핑 없음
   - 인자: `--out` (line 568, `def main` line 562)
   - sink: line 581 `Path(args.out).write_text(text, encoding="utf-8")`
   - 안전 default: line 584 `print(text)`
   - 상태: **★ 신규 Critical7** — 기존 task/proof packet 없음. **§8 트리거 → HOLD_FOR_CHAIR.**

### 2.2 stdout-only 안전 (3건 — 공통 guard 대상 아님)

- **anu_v3/auto_remediation_planner.py** (`task-2612`): argparse·`def main` 이나 출력 인자→sink taint 0. `print(json.dumps(...))` stdout-only. 파일 write sink 0.
- **anu_v3/codex_high_classifier.py** (critical7_classifier 계열): `--input/--rules` 입력 전용. `open()` 은 `'r'` read 한정. stdout-only print.
- **anu_v3/critical7_classifier.py** (critical7_classifier 계열·spec §2 명시 확인): `--input/--rules` 입력 전용. `open()` `'r'` read 한정. write sink 0 → **공통 guard 대상 아님**.

### 2.3 spec §2 명시 대상 task 포함 여부

- `task-2610` → batch_hold_adjudicator.py — **포함**(Critical7 후보)
- `task-2612` → auto_remediation_planner.py — **포함**(stdout-only 안전·guard 불요)
- `task-2613` → batch_dependency_classifier.py — **포함**(established Critical7)
- `task-2615` → **memory/events/task-2615.* 부재 — 미dispatch. file 매핑 없음** (scan 결과 명시)
- `critical7_classifier` 계열(critical7_classifier.py·codex_high_classifier.py) — **포함 확인·둘 다 stdout-only·write sink 0·guard 대상 아님**

## 3. Critical7 여부

3건 모두 Critical7 클래스(forbidden-path / scope-expansion — 무검증 arbitrary fs write). task-2613+1 proof packet 이 이 클래스를 Critical7=CHAIR_HOLD 로 확정. **신규 Critical7 1건(pre_authorized_evidence_bundle_builder.py) → §8 전체 CHAIR_HOLD.**

## 4. 공통 guard 설계 (설계만 — 구현 0·§4)

- **모듈**: `anu_v3/cli_output_path_guard.py` (제안). **import-only 순수 모듈** — argparse/main/CLI **절대 금지**(자기참조 결함 회피).
- **API 스케치**: `validate_output_path(requested, *, task_id, policy) -> Path`(fail-closed) · `atomic_guarded_write(path, data, *, task_id, policy)` · `load_policy(path) -> GuardPolicy`.
- **policy(yaml/json)**: `canonical_ws_root=/home/jay/workspace` · `allowed_roots=[memory/events, memory/reports, memory/fixtures]` · `task_id_prefix_required=true` · deny=[absolute_outside_ws, dotdot_traversal, symlink_component, hardlink, ws_escape, frozen_anchor_overwrite].
- **fail-closed**: 절대경로 ws 이탈 · `../` traversal · symlink · hardlink · workspace 이탈 · overwrite 위험 → 거부.
- **`Path.resolve()` strict-prefix 단독 불충분 — 명시적 가정 금지.** TOCTOU/symlink 방어:
  - 경로 component 별 `os.lstat` → symlink component 거부
  - target dir 내 temp 를 `O_NOFOLLOW|O_EXCL` 로 생성 → 검증 → `os.replace` atomic rename (검증~write TOCTOU 경계 차단)
  - post-resolve 재검증
- **기본 출력 stdout 유지** · 파일 출력은 guard PASS 시에만.

## 5. 모듈별 expected_files / allowed artifact path

- batch_hold_adjudicator.py: `memory/events/task-<id>.json`·`memory/reports/task-<id>.md` (task-id prefix 강제)
- batch_dependency_classifier.py: `memory/events/task-2613*.json`·`memory/reports/task-2613*.md` (task-2613+1 expected_files allowlist 일치)
- pre_authorized_evidence_bundle_builder.py: `memory/events/task-<id>.json` (task-id prefix)

## 6. 수정 예상 파일 목록 (remediation 승인 시·현재 0)

- `anu_v3/batch_hold_adjudicator.py` (sink 라인 한정)
- `anu_v3/batch_dependency_classifier.py` (sink 라인 한정)
- `anu_v3/pre_authorized_evidence_bundle_builder.py` (sink 라인 한정·CHAIR_HOLD)
- `anu_v3/cli_output_path_guard.py` (신규 guard 모듈·설계 승인 시)
- `config/cli_output_path_policy.yaml` (신규 policy·설계 승인 시)
- `tests/test_task2616_cli_output_guard.py` (회귀 harness·설계 승인 시)

## 7. 회귀 테스트 목록

`memory/events/task-2616.regression-candidates.json` RC-2616-01..07 참조 — positive_path / negative_fail_closed / new_critical7 / control_no_change / guard_security_invariant(TOCTOU). harness: real-entrypoint subprocess(mock_only=false)·tmp_path 격리·frozen anchor sha256 diff=0.

## 8. 기존 2610/2613+1 remediation 충돌 여부

- **task-2610**: scope 다름(adjudication 로직). 충돌 아님. 단 동일 파일 byte-0 anchor(`task-2610.result.json` sha256 `f20b52b9…`) 재baseline 필요(회장 승인).
- **task-2613+1**: scope 다름(dependency 로직). 충돌 아님. task-2613+1 이 `--out` Critical7 을 명시적 CHAIR_HOLD 로 보류 → 본 remediation 은 그 HOLD 후속(회장 승인·anchor `93ffc27e…` 재baseline 필수).
- 두 건 모두 로직 byte-불변 분리 입증 필수.

## 9. task-2611+1 label precedence scope 혼합 여부

`C7_OWNER_PAT vs C7_CREDENTIAL` precedence = **별도 트랙**. 본 설계(출력경로 guard) 와 scope 혼합 **0**. Critical7 우선순위/governance 라벨 미접촉. 별도 proof packet 트랙 유지.

## 10. 동결 / HOLD

- 동결 유지: +53/+54/+55/Track E 금지 · Critical7 전건 durable-success write-back 금지 · 추가 dispatch/write/수정 0.
- scan 대상 코드 read-only byte-0 — 검증 완료.
- **신규 Critical7 1건 발견 → §8 HOLD_FOR_CHAIR.** 실제 remediation 은 scan 보고 후 회장 별도 승인.
- 완료 callback = 독립 ANU key `c119085addb0f8b7` only. executor self key `c38fb9955616e24d` callback/collector/adjudication/dispatch 0(+49 코드 정본).
