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

# Plan - EDGE_CONN Edge Connector

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

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

## 배경

Control Plane endpoint가 준비되면 Edge는 설정된 endpoint로 outbound 연결하고 hello/enrollment를 보내야 한다. 이 작업은 Edge config 계약, outbound proto-socket client, runtime lifecycle을 함께 바꾼다. Node bootstrap/local fallback은 유지하고 Control Plane이 Node와 직접 연결되는 경로는 만들지 않는다.

## 사용자 리뷰 요청 흐름

구현 중 차단 사유가 생기면 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/edge/rules.md`
- `agent-ops/rules/project/domain/platform-common/rules.md`
- `agent-test/local/edge-smoke.md`
- `agent-test/local/platform-common-smoke.md`
- `packages/config/config.go`
- `packages/config/config_test.go`
- `packages/hostsetup/templates.go`
- `packages/hostsetup/setup.go`
- `packages/hostsetup/setup_test.go`
- `configs/edge.yaml`
- `apps/edge/internal/bootstrap/runtime.go`
- `apps/edge/internal/bootstrap/runtime_test.go`
- `apps/edge/internal/bootstrap/module.go`
- `apps/edge/internal/transport/server.go`
- `apps/edge/internal/transport/integration_test.go`
- `apps/edge/internal/transport/heartbeat_test.go`
- `apps/node/internal/transport/client.go`
- `apps/node/internal/transport/session.go`
- `go.mod`
- `../proto-socket/go/tcp_client.go`
- `../proto-socket/go/base_client.go`

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

- `packages/config`에는 Edge identity/default/openai/a2a tests가 있으나 Control Plane connector config tests가 없다.
- `packages/hostsetup` template tests check bootstrap/log fields but not Control Plane fields.
- Edge runtime tests cover node store, input manager, listener lifetime, logs, duplicate tokens; no Control Plane connector lifecycle wiring exists.
- No Edge outbound connector package exists, so hello, reconnect, heartbeat/disconnect tests are absent.

### 심볼 참조

- 제거/rename할 심볼 없음.
- `config.EdgeConfig` call sites include `packages/config/config.go:13`, `apps/edge/internal/bootstrap/runtime.go:25`, `apps/edge/internal/input/manager.go`, `apps/edge/cmd/edge/main.go`, tests under `apps/edge/**` and `packages/config/config_test.go`.
- `bootstrap.Runtime` lifecycle call sites: `apps/edge/internal/bootstrap/module.go:15`, `apps/edge/internal/opsconsole/console.go:31`, `apps/edge/internal/bootstrap/runtime_test.go`.

### 분할 판단

Split policy를 먼저 평가했다. 이 subtask는 `agent-task/m-control-plane-edge-wire-baseline/02+01_edge_connector`이며 `01_cp_edge_endpoint`의 `complete.log`가 선행되어야 한다. Config/API foundation과 Edge runtime rollout은 같은 Edge connector ownership 안에 있으므로 이 subtask 안에서 다루되, process-level smoke는 `03+01,02_local_smoke`로 분리한다.

### 범위 결정 근거

이 subtask는 Edge가 Control Plane에 outbound로 붙고, proto-socket heartbeat를 사용하며, disconnect/reconnect를 자체 상태로 갱신하는 것까지 한다. Control Plane public registry API, status snapshot request/response, node lifecycle event relay, Flutter client UI는 제외한다. Edge local node transport, OpenAI/A2A input path, node bootstrap UX는 변경하지 않는다.

### 빌드 등급

build=`cloud-G08`, review=`cloud-G08`. 이유: Edge runtime lifecycle, reconnect loop, shared config/template changes, proto-socket heartbeat behavior가 함께 걸린다.

## 의존 관계 및 구현 순서

이 디렉터리명은 `02+01_edge_connector`다. 같은 task group의 `01_cp_edge_endpoint`가 `complete.log`를 만든 뒤 시작한다.

## 구현 체크리스트

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

### [EDGE_CONN-1] Edge connector config

#### 문제

[packages/config/config.go](/config/workspace/iop/packages/config/config.go:13)의 `EdgeConfig`에는 Control Plane endpoint가 없다. [packages/hostsetup/templates.go](/config/workspace/iop/packages/hostsetup/templates.go:8)와 [configs/edge.yaml](/config/workspace/iop/configs/edge.yaml:1)도 connector 설정을 만들지 않는다.

Before:

```go
// packages/config/config.go:13
type EdgeConfig struct {
	Edge      EdgeInfo          `mapstructure:"edge"      yaml:"edge"`
	Server    EdgeServerConf    `mapstructure:"server"    yaml:"server"`
	Bootstrap EdgeBootstrapConf `mapstructure:"bootstrap" yaml:"bootstrap"`
```

#### 해결 방법

`EdgeControlPlaneConf`를 추가하고 `EdgeConfig`에 `ControlPlane EdgeControlPlaneConf mapstructure:"control_plane" yaml:"control_plane"`를 둔다. 필드는 다음 기준으로 시작한다.

```go
type EdgeControlPlaneConf struct {
	Enabled              bool   `mapstructure:"enabled" yaml:"enabled"`
	WireAddr             string `mapstructure:"wire_addr" yaml:"wire_addr"`
	ReconnectIntervalSec int    `mapstructure:"reconnect_interval_sec" yaml:"reconnect_interval_sec"`
}
```

기본값은 disabled, empty addr, reconnect 5초다. `configs/edge.yaml`, `EdgeSpec().ConfigTemplate`, `EdgeBundleConfigTemplate()`에 `control_plane` section을 추가한다.

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

- [ ] `packages/config/config.go`: config struct/default 추가.
- [ ] `packages/config/config_test.go`: default/override tests 추가.
- [ ] `configs/edge.yaml`: tracked example section 추가.
- [ ] `packages/hostsetup/templates.go`: service/bundle template section 추가.
- [ ] `packages/hostsetup/setup_test.go`: template contains control plane fields assertion 추가.

#### 테스트 작성

테스트 작성. `TestLoadEdge_ControlPlaneDefaults`, `TestLoadEdge_ControlPlaneOverride`, hostsetup template tests에 `control_plane`, `wire_addr`, `reconnect_interval_sec` assertions를 추가한다.

#### 중간 검증

```bash
go test -count=1 ./packages/config ./packages/hostsetup
```

기대: config and hostsetup tests pass.

### [EDGE_CONN-2] Edge outbound connector

#### 문제

현재 Edge runtime은 Node listener/input/artifact만 소유한다. [apps/edge/internal/bootstrap/runtime.go](/config/workspace/iop/apps/edge/internal/bootstrap/runtime.go:24)에 Control Plane connector field가 없고, outbound hello/reconnect state를 담당하는 package도 없다.

Before:

```go
// apps/edge/internal/bootstrap/runtime.go:24
type Runtime struct {
	Cfg       *config.EdgeConfig
	Logger    *zap.Logger
	Registry  *edgenode.Registry
```

#### 해결 방법

`apps/edge/internal/controlplane/connector.go`를 추가한다.

- `Connector`는 `config.EdgeInfo`, `config.EdgeControlPlaneConf`, `version.Version`, logger를 받아 생성한다.
- `Start(ctx)`는 enabled=false 또는 empty `wire_addr`면 no-op한다.
- enabled이면 reconnect loop를 시작하고 `proto_socket.DialTcp`로 연결한다.
- 연결 직후 `EdgeHelloRequest{EdgeId, EdgeName, Version, Capabilities}`를 보내고 accepted response를 확인한다.
- proto-socket heartbeat는 `DialTcp` interval/wait로 켠다. interval/wait는 edge-node와 같은 30/45초 기준을 우선한다.
- disconnect listener는 connector state를 disconnected로 바꾸고 reconnect interval 후 다시 시도한다.
- `Stop()`은 loop context cancel과 active client close를 수행한다.

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

- [ ] `apps/edge/internal/controlplane/connector.go`: connector, state, reconnect loop 구현.
- [ ] `apps/edge/internal/controlplane/connector_test.go`: fake proto-socket TCP server 기반 hello/reconnect/stop tests 추가.
- [ ] `apps/edge/internal/controlplane/heartbeat_test.go`: wait > interval invariant test 추가.

#### 테스트 작성

테스트 작성. `TestConnectorSendsEdgeHello`, `TestConnectorRejectsRejectedHello`, `TestConnectorReconnectsAfterDisconnect`, `TestConnectorStopClosesConnection`, `TestHeartbeatWaitExceedsInterval`를 추가한다. Fake Control Plane server는 `proto_socket.NewTcpServer`와 `iop.EdgeHelloRequest/Response`를 사용한다.

#### 중간 검증

```bash
go test -count=1 ./apps/edge/internal/controlplane
```

기대: connector unit tests pass without external service.

### [EDGE_CONN-3] Runtime lifecycle wiring

#### 문제

[apps/edge/internal/bootstrap/runtime.go](/config/workspace/iop/apps/edge/internal/bootstrap/runtime.go:79)는 Edge node server, input manager, artifact server만 start한다. Connector가 추가되어도 runtime lifecycle에 연결되지 않으면 `iop-edge serve`가 Control Plane에 붙지 않는다.

Before:

```go
// apps/edge/internal/bootstrap/runtime.go:79
func (r *Runtime) Start(ctx context.Context) error {
	if err := ctx.Err(); err != nil {
		return err
	}
	r.wireHandlers()
	lifetimeCtx := r.newLifetimeContext()
	if err := r.Server.Start(lifetimeCtx); err != nil {
```

#### 해결 방법

`Runtime`에 `ControlPlane *controlplane.Connector`를 추가하고 `NewRuntime`에서 생성한다. `Start()`는 node server/input/artifact 성공 후 connector를 start한다. connector start가 synchronous dial failure를 반환하지 않는 설계라면 no-op/reconnect loop start만 검증한다. `Stop()`은 connector stop을 input/artifact/server stop과 함께 error join 방식 또는 기존 precedence에 맞춰 반영한다.

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

- [ ] `apps/edge/internal/bootstrap/runtime.go`: field/create/start/stop 연결.
- [ ] `apps/edge/internal/bootstrap/runtime_test.go`: disabled no-op, enabled connector presence, lifecycle start/stop tests 추가.
- [ ] `apps/edge/internal/bootstrap/module.go`: 변경 필요 시 import/lifecycle 영향 확인.

#### 테스트 작성

테스트 작성. `TestNewRuntimeWiresControlPlaneConnector`, `TestRuntimeStartsControlPlaneConnectorWhenEnabled`, `TestRuntimeStopsControlPlaneConnector`를 추가한다. 외부 Control Plane은 fake connector server 또는 connector package의 test helper를 직접 import하지 않고, local TCP fake endpoint를 사용한다.

#### 중간 검증

```bash
go test -count=1 ./apps/edge/internal/bootstrap
```

기대: runtime lifecycle tests pass.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `packages/config/config.go` | EDGE_CONN-1 |
| `packages/config/config_test.go` | EDGE_CONN-1 |
| `configs/edge.yaml` | EDGE_CONN-1 |
| `packages/hostsetup/templates.go` | EDGE_CONN-1 |
| `packages/hostsetup/setup_test.go` | EDGE_CONN-1 |
| `apps/edge/internal/controlplane/connector.go` | EDGE_CONN-2 |
| `apps/edge/internal/controlplane/connector_test.go` | EDGE_CONN-2 |
| `apps/edge/internal/controlplane/heartbeat_test.go` | EDGE_CONN-2 |
| `apps/edge/internal/bootstrap/runtime.go` | EDGE_CONN-3 |
| `apps/edge/internal/bootstrap/runtime_test.go` | EDGE_CONN-3 |
| `apps/edge/internal/bootstrap/module.go` | EDGE_CONN-3 |

## 최종 검증

```bash
go test -count=1 ./packages/config ./packages/hostsetup ./apps/edge/internal/controlplane ./apps/edge/internal/bootstrap
```

기대: Edge connector config, connector, runtime lifecycle tests pass with fresh execution. Go test cache output is not acceptable for this plan; use `-count=1`.

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