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

# Plan - EDGE_CONN Control Plane Edge Endpoint

## 이 파일을 읽는 구현 에이전트에게

구현의 마지막 단계는 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 내용과 검증 출력으로 채우는 것이다. 검증을 실행하고, 실제 stdout/stderr를 붙이고, active 파일을 유지한 채 리뷰 준비를 보고한다. 사용자만 결정할 수 있는 범위 충돌, 외부 환경/secret 준비, 제품 결정이 필요하면 review stub의 `사용자 리뷰 요청`을 근거와 함께 채우고 멈춘다. 단순 증거 공백은 후속 에이전트가 재실행할 수 있으므로 사용자 리뷰 요청으로 올리지 않는다.

## 배경

두 번째 Epic `edge-connector`는 Edge outbound 연결 전에 Control Plane이 받을 native TCP endpoint를 필요로 한다. 현재 Control Plane의 `wire_listen`은 Client WebSocket `/client` 서버에만 쓰이고 있어 Edge TCP wire와 같은 port에서 공존할 수 없다. 먼저 Control Plane에 별도 Edge TCP wire endpoint와 hello 기반 registry foundation을 만든다.

## 사용자 리뷰 요청 흐름

구현 중 차단 사유가 생기면 active `CODE_REVIEW-*-G??.md`의 `사용자 리뷰 요청` 섹션을 사용한다. code-review가 요청 타당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.

## 분석 결과

### 읽은 파일

- `agent-roadmap/current.md`
- `agent-roadmap/phase/control-plane-portal-ops/PHASE.md`
- `agent-roadmap/phase/control-plane-portal-ops/milestones/control-plane-edge-wire-baseline.md`
- `agent-ops/rules/project/domain/control-plane/rules.md`
- `agent-ops/rules/project/domain/platform-common/rules.md`
- `agent-test/local/control-plane-smoke.md`
- `agent-test/local/platform-common-smoke.md`
- `apps/control-plane/cmd/control-plane/main.go`
- `apps/control-plane/cmd/control-plane/main_test.go`
- `apps/control-plane/internal/wire/wire.go`
- `apps/control-plane/internal/wire/client.go`
- `apps/control-plane/internal/wire/client_test.go`
- `apps/control-plane/internal/wire/edge.go`
- `apps/control-plane/internal/wire/edge_test.go`
- `configs/control-plane.yaml`
- `go.mod`
- `../proto-socket/go/tcp_server.go`
- `../proto-socket/go/tcp_client.go`
- `../proto-socket/go/base_client.go`

### 테스트 커버리지 공백

- Control Plane config loading은 `main_test.go`가 `wire_listen` override만 검증한다. `edge_wire_listen` default/YAML/env 테스트가 없다.
- `apps/control-plane/internal/wire/edge_test.go`는 proto roundtrip만 검증한다. 실제 `proto_socket.NewTcpServer` hello request/response와 disconnect registry 갱신 테스트가 없다.
- HTTP health/readiness lifecycle은 기존 `go test ./apps/control-plane/...` 범위에 남는다.

### 심볼 참조

- 제거/rename할 심볼 없음.
- `WireListen` call site: `apps/control-plane/cmd/control-plane/main.go:26`, `main.go:89`, `main.go:116`, `main.go:141`, `main.go:152`, `main_test.go:171`, `main_test.go:186`, `configs/control-plane.yaml:3`.
- `EdgeParserMap` call site: `apps/control-plane/internal/wire/edge_test.go:15`, `edge_test.go:67`.

### 분할 판단

Split policy를 먼저 평가했다. 이 작업은 shared task group `m-control-plane-edge-wire-baseline` 아래의 split 작업이다.

- `01_cp_edge_endpoint`: Control Plane native TCP endpoint와 registry foundation. 선행 없음.
- `02+01_edge_connector`: Edge config/outbound connector/runtime lifecycle. `01_cp_edge_endpoint` 완료 후 시작한다.
- `03+01,02_local_smoke`: 실제 Control Plane + Edge process smoke. `01`, `02` 완료 후 시작한다.

분할 사유: Control Plane endpoint와 Edge connector가 서로 다른 domain ownership이고, 마지막 smoke는 process orchestration/terminal-agent 성격이므로 별도 검증 단위가 필요하다.

### 범위 결정 근거

이 subtask는 Control Plane이 Edge hello를 받을 수 있는 endpoint와 내부 connection state까지만 만든다. Edge outbound connector, Edge config, smoke script, public registry API, status snapshot, event relay는 후속 subtask 또는 다른 Epic으로 둔다. Node 직접 연결/스케줄링 계약은 만들지 않는다.

### 빌드 등급

build=`cloud-G08`, review=`cloud-G08`. 이유: protocol endpoint, heartbeat/disconnect lifecycle, config split이 포함된 Control Plane wire 변경이다.

## 구현 체크리스트

- [ ] [EDGE_CONN-1] Control Plane config에서 Client WS wire listen과 Edge TCP wire listen을 분리하고 둘 다 lifecycle에서 시작한다.
- [ ] [EDGE_CONN-2] Control Plane Edge TCP server와 internal edge connection registry foundation을 구현하고 hello/disconnect 테스트를 추가한다.
- [ ] 중간/최종 검증 명령을 실행하고 출력이 기대와 일치하는지 확인한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [EDGE_CONN-1] Control Plane config split

#### 문제

[apps/control-plane/cmd/control-plane/main.go](/config/workspace/iop/apps/control-plane/cmd/control-plane/main.go:23)는 `server.wire_listen` 하나만 갖고, [main.go](/config/workspace/iop/apps/control-plane/cmd/control-plane/main.go:152)에서 그 주소를 Client WebSocket server에 사용한다. Edge TCP server를 같은 주소에 bind하면 충돌한다.

Before:

```go
// apps/control-plane/cmd/control-plane/main.go:23
type controlPlaneConfig struct {
	Server struct {
		Listen     string `yaml:"listen"`
		WireListen string `yaml:"wire_listen"`
	} `yaml:"server"`
```

```go
// apps/control-plane/cmd/control-plane/main.go:152
clientServer, err := wire.NewClientServer(cfg.Server.WireListen, logger)
```

#### 해결 방법

`WireListen`은 기존 Client WebSocket 경계로 유지하고 `EdgeWireListen string yaml:"edge_wire_listen"`을 추가한다. 기본값은 `0.0.0.0:19081`로 둔다. `IOP_EDGE_WIRE_LISTEN` env override를 추가한다. `configs/control-plane.yaml`에 `edge_wire_listen`을 추가한다. `run()`은 ClientServer와 새 EdgeServer를 각각 시작하고 shutdown 시 둘 다 stop한다.

After shape:

```go
type controlPlaneConfig struct {
	Server struct {
		Listen         string `yaml:"listen"`
		WireListen     string `yaml:"wire_listen"`
		EdgeWireListen string `yaml:"edge_wire_listen"`
	} `yaml:"server"`
}
```

#### 수정 파일 및 체크리스트

- [ ] `apps/control-plane/cmd/control-plane/main.go`: config field/default/env override/start lifecycle 추가.
- [ ] `apps/control-plane/cmd/control-plane/main_test.go`: default, YAML, `IOP_EDGE_WIRE_LISTEN` override 테스트 추가.
- [ ] `configs/control-plane.yaml`: `server.edge_wire_listen` 예시 추가.

#### 테스트 작성

테스트 작성. `TestLoadConfigEdgeWireListenDefault`, `TestLoadConfigEdgeWireListenEnvOverrides`를 추가하고 기존 `TestLoadConfigRepositoryLocalConfig`에서 tracked config가 edge wire listen을 로드하는지 확인한다.

#### 중간 검증

```bash
go test -count=1 ./apps/control-plane/cmd/control-plane
```

기대: config loading tests pass.

### [EDGE_CONN-2] Edge TCP server and registry foundation

#### 문제

[apps/control-plane/internal/wire/edge.go](/config/workspace/iop/apps/control-plane/internal/wire/edge.go:12)는 parser map만 제공한다. TCP server, hello handler, connected/disconnected state 보관이 없다.

Before:

```go
// apps/control-plane/internal/wire/edge.go:12
func EdgeParserMap() proto_socket.ParserMap {
	return proto_socket.ParserMap{
```

#### 해결 방법

`apps/control-plane/internal/wire/edge_server.go`와 `edge_registry.go`를 추가한다.

- `EdgeServer`는 `proto_socket.NewTcpServer`로 `EdgeTransport` endpoint를 bind한다.
- 연결마다 `proto_socket.NewTcpClient(conn, 30, 45, EdgeParserMap())`을 만든다.
- `EdgeHelloRequest` listener가 `edge_id` empty를 reject하고, accepted response에 `Protocol`과 server time을 채운다.
- `EdgeRegistry`는 `edge_id`, `edge_name`, capabilities, protocol, connected, last seen, disconnect info를 mutex로 보관한다.
- disconnect listener는 connected=false와 last seen update를 기록한다.

#### 수정 파일 및 체크리스트

- [ ] `apps/control-plane/internal/wire/edge_server.go`: TCP server, hello handler, disconnect listener 구현.
- [ ] `apps/control-plane/internal/wire/edge_registry.go`: internal registry model과 snapshot helpers 구현.
- [ ] `apps/control-plane/internal/wire/edge_test.go`: 기존 parser tests 유지.
- [ ] `apps/control-plane/internal/wire/edge_server_test.go`: hello accepted/rejected, disconnect state 테스트 추가.
- [ ] `apps/control-plane/internal/wire/wire.go`: heartbeat constants가 필요하면 이 패키지 안에서 명확히 둔다.

#### 테스트 작성

테스트 작성. `TestEdgeServerHelloRegistersEdge`, `TestEdgeServerRejectsMissingEdgeID`, `TestEdgeServerDisconnectMarksEdgeDisconnected`, `TestEdgeHeartbeatWaitExceedsInterval`를 추가한다.

#### 중간 검증

```bash
go test -count=1 ./apps/control-plane/internal/wire
```

기대: parser, client WS, edge TCP server tests pass.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/control-plane/cmd/control-plane/main.go` | EDGE_CONN-1 |
| `apps/control-plane/cmd/control-plane/main_test.go` | EDGE_CONN-1 |
| `configs/control-plane.yaml` | EDGE_CONN-1 |
| `apps/control-plane/internal/wire/edge_server.go` | EDGE_CONN-2 |
| `apps/control-plane/internal/wire/edge_registry.go` | EDGE_CONN-2 |
| `apps/control-plane/internal/wire/edge_server_test.go` | EDGE_CONN-2 |
| `apps/control-plane/internal/wire/edge_test.go` | EDGE_CONN-2 |
| `apps/control-plane/internal/wire/wire.go` | EDGE_CONN-2 |

## 최종 검증

```bash
go test -count=1 ./apps/control-plane/...
```

기대: Control Plane command and wire package tests pass with fresh execution. Go test cache output is not acceptable for this plan; use `-count=1`.

모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
