<!-- task=01_setup_cli_core plan=2 tag=SETUPCORE-FIX2 -->

# Code Review Reference - SETUPCORE-FIX2

> **[IMPLEMENTING AGENT — READ FIRST]** 이 파일의 구현 에이전트 소유 섹션을 채우기 전에는 작업이 완료된 것이 아니다.
> `코드리뷰 전용 체크리스트`는 리뷰 에이전트만 수정한다.

## 개요

date=2026-05-20
task=01_setup_cli_core, plan=2, tag=SETUPCORE-FIX2

## 구현 항목별 완료 여부

| 항목 | 완료 여부 |
|------|---------|
| [SETUPCORE-FIX2-1] nested data dir 부모 접근 보강 | [x] |
| [SETUPCORE-FIX2-2] recursive chown 안전장치 추가 | [x] |
| [SETUPCORE-FIX2-3] 단위 테스트 추가 | [x] |

## 구현 체크리스트

- [x] [SETUPCORE-FIX2-1] 기본 nested data dir의 부모 경로까지 서비스 사용자가 traverse 가능하도록 권한/소유권 처리를 보강한다.
- [x] [SETUPCORE-FIX2-2] recursive chown 대상 data dir에 안전장치를 추가한다.
- [x] [SETUPCORE-FIX2-3] hostsetup 단위 테스트로 nested data dir와 unsafe data dir rejection을 검증한다.
- [x] 중간 검증과 최종 검증 명령을 실행하고 실제 출력을 기록한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다.

## 코드리뷰 전용 체크리스트

> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.

- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G07_N.log`로 아카이브한다.
- [x] active `PLAN-*-G??.md`를 `plan_cloud_G07_M.log`로 아카이브한다.
- [ ] PASS이면 `complete.log`를 작성하고 task 디렉터리를 archive로 이동한다.

## 계획 대비 변경 사항

- `ensureDirs`를 분리: data dir의 _부모_는 `0755`로 먼저 만들고, _leaf_만 `0750`으로 만든다. 결과적으로 `/var/lib/iop`는 `drwxr-xr-x root:root`, `/var/lib/iop/edge`는 `drwxr-x--- iop:iop`가 되어 systemd `User=iop` 프로세스가 부모를 traverse하고 leaf로 진입한다. 부모를 chown하지 않은 이유: `/var/lib/iop`는 edge/node가 공유하는 root-owned 디렉터리이며 소유권은 root 그대로 두고 0755의 traverse bit만 보장하는 편이 단순/안전하다.
- `validateDataDir`를 추가했다. `filepath.Clean` 후 `unsafeDataDirs`(`/`, `/etc`, `/home`, `/opt`, `/root`, `/srv`, `/tmp`, `/usr`, `/usr/lib`, `/usr/local`, `/var`, `/var/lib`, `/var/log`, `/var/run`) 집합에 일치하면 `Run` 시작 단계에서 거부한다. 계획의 "안전한 앱 전용 경로인지 검증" 요구를 만족하면서, dry-run을 포함한 모든 경로에서 동작해 외부 명령/파일 시스템 변경이 발생하기 전에 차단한다.
- 안전장치 검증은 `TestRunRejectsUnsafeDataDir`에서 7개 경로(`/`, `/var`, `/var/lib`, `/etc`, `/usr`, `/usr/lib`, `/var/log`)에 대해 수행한다. `runner.calls` 빈 상태도 함께 검증해 거부 전 외부 명령이 호출되지 않음을 보장한다.

## 주요 설계 결정

- 부모 디렉터리 권한 정합성은 `ensureDirs`가 책임진다. `chown -R`은 여전히 leaf만 대상으로 하므로 blast radius가 leaf로 제한된다.
- unsafe data dir 검증은 chown 호출 이전, `applyDefaults` 직후로 위치시켰다. dry-run에서도 적용되어 운영자가 잘못된 입력을 빠르게 인지하도록 한다.
- `filepath.Clean`을 사용하므로 `/var/lib/`, `/var//lib`, `/var/lib/.` 같은 표면적 변형도 동일하게 차단된다. 절대 경로 가정은 그대로(테스트 spec의 tmp dir 기반 path는 unsafe 목록에 없어 정상 통과).

## 리뷰어를 위한 체크포인트

- fresh host에서 `/var/lib/iop/edge`와 `/var/lib/iop/node` 부모 경로를 `iop` 서비스 사용자가 traverse할 수 있는지 확인한다.
- unsafe data dir가 recursive chown 전에 거부되는지 확인한다.
- 기존 config no-overwrite와 dry-run 동작이 유지되는지 확인한다.

## 검증 결과

### 중간 검증
```
$ gofmt -w packages/hostsetup
$ go test -count=1 ./packages/hostsetup
ok  	iop/packages/hostsetup	0.003s
```

### 최종 검증
```
$ go test -count=1 ./packages/config ./packages/hostsetup
ok  	iop/packages/config	0.006s
ok  	iop/packages/hostsetup	0.003s
```

## 코드리뷰 결과

- 종합 판정: FAIL
- Required: `packages/hostsetup/setup.go:224`-`230`는 data dir 부모를 `os.MkdirAll(parent, 0755)`로 생성하지만, `MkdirAll`은 이미 존재하는 디렉터리의 mode를 교정하지 않는다. 이전 실패 구현이나 수동 설치로 `/var/lib/iop` 같은 shared parent가 이미 root:root `0750`이면 이번 setup을 다시 실행해도 parent는 계속 non-traversable이고, `packages/hostsetup/setup.go:262`-`268`의 `chown -R`은 leaf data dir에만 적용되므로 systemd `User=iop` 프로세스가 `/var/lib/iop/edge`까지 진입하지 못한다. `packages/hostsetup/setup_test.go:228`-`265`도 fresh 생성 케이스만 확인해서 이 회귀를 잡지 못한다. parent가 이미 존재하는 경우에도 traverse bit를 보장하도록 chmod/chown 정책을 명시하고, 기존 `0750` parent를 교정하는 테스트를 추가해야 한다.
- 검증 재현:
  - `gofmt -l packages/hostsetup` → 출력 없음
  - `go test -count=1 ./packages/hostsetup` → `ok  	iop/packages/hostsetup	0.003s`
  - `go test -count=1 ./packages/config ./packages/hostsetup` → `ok  	iop/packages/config	0.006s`, `ok  	iop/packages/hostsetup	0.003s`
- 다음 단계: FAIL이므로 `complete.log` 작성 및 task archive 이동은 진행하지 않는다. 위 Required 이슈를 수정한 뒤 재리뷰한다.
