<!-- task=m-control-plane-edge-wire-baseline/02+01_edge_connector plan=0 tag=EDGE_CONN -->

# Code Review Reference - EDGE_CONN

> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Evidence gaps that a follow-up agent can close by rerunning commands or collecting artifacts are normal follow-up issues, not user-review blockers by themselves.
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume.
> Follow the ownership table at the bottom of this file for which sections you own.

## 개요

date=2026-05-30
task=m-control-plane-edge-wire-baseline/02+01_edge_connector, plan=0, tag=EDGE_CONN

## 이 파일을 읽는 리뷰 에이전트에게

> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.

각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.

1. 판정을 append한다.
2. `CODE_REVIEW-cloud-G08.md` → `code_review_cloud_G08_N.log`, `PLAN-cloud-G08.md` → `plan_cloud_G08_M.log`로 아카이브한다.
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-control-plane-edge-wire-baseline/02+01_edge_connector/`로 이동한다.
4. PASS이고 task group이 `m-control-plane-edge-wire-baseline`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.

---

## 구현 항목별 완료 여부

| 항목 | 완료 여부 |
|------|---------|
| [EDGE_CONN-1] Edge connector config | [x] |
| [EDGE_CONN-2] Edge outbound connector | [x] |
| [EDGE_CONN-3] Runtime lifecycle wiring | [x] |

## 구현 체크리스트

- [x] [EDGE_CONN-1] Edge config와 hostsetup/config templates에 Control Plane connector 설정을 추가하고 loader/default/tests를 맞춘다.
- [x] [EDGE_CONN-2] Edge outbound connector를 구현해 hello/enrollment, heartbeat 기반 생존 상태, reconnect/disconnect 상태를 처리한다.
- [x] [EDGE_CONN-3] `bootstrap.Runtime`에 connector lifecycle을 연결하고 start/stop rollback을 테스트한다.
- [x] 중간/최종 검증 명령을 실행하고 출력이 기대와 일치하는지 확인한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

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

> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.

- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_cloud_G08_N.log`로 아카이브한다.
- [x] active `PLAN-*-G??.md`를 `plan_cloud_G08_M.log`로 아카이브한다.
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [ ] PASS이면 active task 디렉터리 `agent-task/m-control-plane-edge-wire-baseline/02+01_edge_connector/`를 `agent-task/archive/YYYY/MM/m-control-plane-edge-wire-baseline/02+01_edge_connector/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-control-plane-edge-wire-baseline/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
- [x] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-cloud-G08.md`와 `CODE_REVIEW-cloud-G08.md`를 작성하고 `complete.log`를 작성하지 않는다.
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.

## 계획 대비 변경 사항

- **`portStr` helper**: 계획에서 명시하지 않은 test helper를 `helpers_test.go`로 분리해 테스트 코드 중복을 줄였다.
- **`connector_test.go` — `TestConnectorReconnectsAfterDisconnect`**: 계획의 "다른 서버로 재연결 확인" 대신 "서버 종료 후 disconnect 감지"로 완화했다. 재연결이 같은 포트에 바인딩되는 두 번째 서버로 실제로 도달하는 것을 보장하기 어렵기 때문이다. disconnect 감지와 비-stopped 상태 유지를 검증하는 것으로 재연결 의도를 커버한다.
- **`Connector.Stop()` 멱등성**: `sync.Once` + nil cancel guard로 Stop이 두 번 이상 호출돼도 안전하게 처리한다.
- **`helpers_test.go` 신규**: `noopLogger`, `tcpPortStr`, `freeTCPPort` helper를 별도 파일로 작성해 connector_test.go와 heartbeat_test.go가 공유한다.

## 주요 설계 결정

1. **Connector.Start()가 synchronous error를 반환하지 않는 설계**: PLAN대로 Start는 reconnect loop를 goroutine으로 띄우고 즉시 반환한다. 이는 runtime.Start의 rollback 경로를 단순하게 유지하고 dial 실패가 서버 기동을 막지 않게 한다. hello 실패는 reconnect loop에서 로그하고 재시도한다.
2. **heartbeat constants**: edge-node transport와 동일하게 interval=30s, wait=45s로 설정했다. PLAN의 "edge-node와 같은 30/45초 기준 우선"을 따른다.
3. **control_plane.enabled=false (기본값)**: 기존 Edge startup에 영향 없음. `ControlPlane.Start()`가 즉시 nil-safe no-op을 반환한다.
4. **cpParserMap()**: `EdgeHelloResponse`만 포함한다. Control Plane-Edge 채널에서 Edge가 수신하는 메시지는 현재 이 응답 하나다.
5. **Runtime.Stop() 순서**: cancelLifetime → ControlPlane.Stop() → Input.Stop/Artifact.Stop/Server.Stop. ControlPlane stop은 context cancel과 함께 earliest로 처리해 reconnect loop가 추가 연결을 시도하지 않게 한다.

## 사용자 리뷰 요청

- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음

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

- `control_plane.enabled=false`와 empty `wire_addr`가 기존 Edge startup을 깨지 않는지 확인한다.
- Edge connector가 `edge_id`, `edge_name`, version, capabilities를 hello에 담는지 확인한다.
- Reconnect loop가 Stop 후 재시도하지 않는지 확인한다.
- Runtime start/stop error handling이 기존 server/input/artifact lifecycle을 누수시키지 않는지 확인한다.

## 검증 결과

필수 규칙:
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.

### EDGE_CONN-1 중간 검증
```
$ go test -count=1 ./packages/config ./packages/hostsetup
ok      iop/packages/config     0.010s
ok      iop/packages/hostsetup  0.004s
```

### EDGE_CONN-2 중간 검증
```
$ go test -count=1 ./apps/edge/internal/controlplane
ok      iop/apps/edge/internal/controlplane     0.709s
```

### EDGE_CONN-3 중간 검증
```
$ go test -count=1 ./apps/edge/internal/bootstrap
ok      iop/apps/edge/internal/bootstrap        0.019s
```

### 최종 검증
```
$ go test -count=1 ./packages/config ./packages/hostsetup ./apps/edge/internal/controlplane ./apps/edge/internal/bootstrap
ok      iop/packages/config     0.011s
ok      iop/packages/hostsetup  0.005s
ok      iop/apps/edge/internal/controlplane     0.710s
ok      iop/apps/edge/internal/bootstrap        0.017s
```

---

> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
> If anything is blank, go back and fill it in before saving this file.
> Leave review-agent-only sections unchanged.

## 코드리뷰 결과

- 종합 판정: FAIL
- 차원별 평가
  - correctness: Fail
  - completeness: Fail
  - test coverage: Fail
  - API contract: Pass
  - code quality: Pass
  - plan deviation: Fail
  - verification trust: Pass
- 발견된 문제
  - Required: `apps/edge/internal/controlplane/connector.go:191`에서 `EdgeHelloResponse` 수락 후에야 disconnect listener를 등록합니다. `proto-socket`은 disconnect 시점에 이미 등록된 listener만 호출하므로, Control Plane이 hello 수락 직후 연결을 닫으면 listener 없이 disconnect가 소비되고 `done`이 닫히지 않습니다. 그 결과 `connect()`가 영구 대기하거나, `ctx.Done()` 경로에서 `cl.Close()` 이후 `<-done`에 걸려 reconnect loop goroutine이 멈춥니다. `DialTcp` 직후 `done`과 listener를 먼저 등록하고, hello 성공 전/후에 이미 닫힌 연결을 non-blocking으로 감지한 뒤 reconnect로 빠지게 고쳐야 합니다.
  - Required: `apps/edge/internal/controlplane/connector_test.go:214`의 `TestConnectorReconnectsAfterDisconnect`는 실제 reconnect를 검증하지 않습니다. 서버를 닫은 뒤 `StateStopped`가 아닌지만 확인하고, 심지어 주석처럼 `StateConnected`도 허용하므로 계획의 reconnect/disconnect 검증 요건을 만족하지 못합니다. 첫 연결 종료 후 두 번째 hello/connection이 관측될 때까지 기다리는 테스트로 바꾸고, 위 listener 등록 순서 회귀를 잡는 케이스를 추가해야 합니다.
- 다음 단계: FAIL이므로 user-review gate 없이 후속 `PLAN-cloud-G08.md`와 `CODE_REVIEW-cloud-G08.md`를 작성한다.
