<!-- task=01_setup_cli_core plan=1 tag=SETUPCORE-FIX -->

# Code Review Reference - SETUPCORE-FIX

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

## 개요

date=2026-05-20
task=01_setup_cli_core, plan=1, tag=SETUPCORE-FIX

## 구현 항목별 완료 여부

| 항목 | 완료 여부 |
|------|---------|
| [SETUPCORE-FIX-1] 서비스 사용자 접근 권한 수정 | [x] |
| [SETUPCORE-FIX-2] ownership/권한 테스트 추가 | [x] |

## 구현 체크리스트

- [x] [SETUPCORE-FIX-1] 서비스 사용자가 config와 data dir에 접근할 수 있게 ownership/권한 처리를 수정한다.
- [x] [SETUPCORE-FIX-2] hostsetup 단위 테스트로 ownership/권한 처리를 검증한다.
- [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로 이동한다.

## 계획 대비 변경 사항

- 계획의 "ownership/권한 처리"를 `chown user:group` 외부 명령을 통해 수행하도록 구현했다. `os.Chown`을 직접 호출하지 않은 이유는 (1) 테스트 환경에서 실제 uid/gid를 만들지 않고도 호출 검증이 가능하고, (2) 기존 `CommandRunner` 추상화와 일관되며, (3) `useradd`가 만든 사용자/그룹을 이름으로 그대로 참조할 수 있어 별도 lookup이 필요 없기 때문이다.
- data dir은 재귀 chown(`chown -R`)으로 처리해 이미 존재하던 하위 파일도 서비스 사용자 소유로 정렬한다. config 파일은 단일 chown으로 처리한다(파일이라 -R 불필요).
- chown은 `OverwriteConfig` 여부와 무관하게 항상 호출한다. 이유: setup은 멱등적이어야 하며, 기존 config 파일의 소유자가 어긋난 상태에서도 다음 setup 실행이 이를 교정해야 한다. 내용은 보존되고 소유권만 강제된다.

## 주요 설계 결정

- 공통 패키지는 `apps/*/internal`을 import하지 않고 `os`, `os/exec`, 표준 라이브러리만 의존한다.
- ownership 적용은 `writeUnit` 이후의 마지막 파일시스템 단계로 두고, 그 다음 `systemctl daemon-reload`/`enable`/`start`를 호출한다. 서비스가 실제 기동되기 전에 권한 정합성이 보장된다.
- dry-run은 chown을 포함해 어떠한 외부 명령이나 파일 시스템 변경도 수행하지 않는다(`runner.calls`가 비어 있어야 함을 기존 테스트가 보장).

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

- `iop` systemd 사용자 기준으로 config 파일과 data dir 접근이 가능한지 확인한다.
- 기존 config no-overwrite 동작이 유지되는지 확인한다.
- dry-run이 파일 쓰기와 system 명령을 실행하지 않는지 확인한다.

## 검증 결과

### 중간 검증
```
$ 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.007s
ok  	iop/packages/hostsetup	0.003s
```

## 코드리뷰 결과

- 종합 판정: FAIL
- Required: `packages/hostsetup/templates.go:40`와 `packages/hostsetup/templates.go:51`의 기본 data dir은 `/var/lib/iop/edge`, `/var/lib/iop/node`처럼 한 단계 더 중첩되어 있다. 그런데 `packages/hostsetup/setup.go:193`-`195`는 `os.MkdirAll(opts.DataDir, 0750)`를 호출하므로 fresh host에서 `/var/lib/iop` 같은 중간 디렉터리도 root:root `0750`으로 생성될 수 있다. 이후 `packages/hostsetup/setup.go:226`-`231`은 leaf data dir만 `chown -R iop:iop` 하므로, systemd의 `User=iop` 프로세스가 부모 `/var/lib/iop`를 traverse하지 못해 `WorkingDirectory=/var/lib/iop/edge` 진입이 실패할 수 있다. 현재 `packages/hostsetup/setup_test.go:65`-`71`의 test spec은 data dir 자체를 `.../var/lib/iop`로 두어서 실제 기본 경로의 부모 디렉터리 권한 문제를 검출하지 못한다. 기본 Edge/Node 경로와 같은 nested data dir 테스트를 추가하고, 부모 경로까지 서비스 사용자가 traverse 가능하도록 권한/소유권 처리를 보강해야 한다.
- Required: `packages/hostsetup/setup.go:226`-`231`의 `chown -R`은 `SetupOptions.DataDir`를 그대로 재귀 적용한다. 이 값은 후속 setup CLI에서 사용자 입력으로 들어갈 수 있는 설치 경로이므로 `/`, `/var`, `/var/lib`, `/etc` 같은 상위 경로가 들어오면 root 권한으로 대규모 소유권 변경이 발생한다. 재귀 chown 전에 data dir가 안전한 앱 전용 경로인지 검증하거나, 재귀 범위를 생성/관리 대상 경로로 제한해야 한다.
- 검증 재현:
  - `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.005s`
- 다음 단계: FAIL이므로 `complete.log` 작성 및 task archive 이동은 진행하지 않는다. 위 Required 이슈를 수정한 뒤 재리뷰한다.
