<!-- task=flutter_first_portal_migration/04_control_plane_ws_endpoint plan=2 tag=REVIEW_REVIEW_CP_WS -->

# Plan - REVIEW_REVIEW_CP_WS

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

구현 마지막 단계에서 `CODE_REVIEW-cloud-G08.md`의 구현 에이전트 소유 섹션을 반드시 실제 변경 내용과 검증 출력으로 채운다. 검증 명령을 실행하고 실제 stdout/stderr를 기록한 뒤 active 파일은 그대로 두고 리뷰 준비 상태만 보고한다. `complete.log` 작성, `.md` 파일 로그화, archive 이동은 code-review-skill 전용이다.

## 배경

두 번째 리뷰에서 follow-up 구현이 아직 반영되지 않았고, active `CODE_REVIEW`도 구현 전 stub 상태인 것이 확인됐다. 원 Required였던 browser-origin WS handshake도 여전히 `nhooyr` 기본 Origin 검증으로 403을 반환한다. 이번 계획은 같은 기술 범위를 유지하되, 구현 완료 기록과 검증 출력까지 반드시 채우도록 분리해 고정한다.

## 분석 결과

### 읽은 파일

- `agent-task/flutter_first_portal_migration/04_control_plane_ws_endpoint/plan_cloud_G08_1.log`
- `agent-task/flutter_first_portal_migration/04_control_plane_ws_endpoint/code_review_cloud_G08_1.log`
- `agent-task/flutter_first_portal_migration/04_control_plane_ws_endpoint/plan_cloud_G08_0.log`
- `agent-task/flutter_first_portal_migration/04_control_plane_ws_endpoint/code_review_cloud_G08_0.log`
- `apps/control-plane/internal/wire/portal.go`
- `apps/control-plane/internal/wire/portal_test.go`
- `apps/control-plane/internal/wire/wire.go`
- `apps/control-plane/cmd/control-plane/main.go`
- `apps/control-plane/cmd/control-plane/main_test.go`
- `apps/portal/lib/portal_config.dart`
- `go.mod`
- `../proto-socket/go/ws_server.go`
- `../proto-socket/go/ws_client.go`
- `../proto-socket/go/communicator.go`
- `../proto-socket/go/test/ws_test.go`
- `/config/go/pkg/mod/nhooyr.io/websocket@v1.8.17/accept.go`
- `/config/go/pkg/mod/nhooyr.io/websocket@v1.8.17/dial.go`

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

- `apps/control-plane/internal/wire/portal_test.go`에는 Origin 없는 Go proto-socket client handshake만 있다.
- `../proto-socket/go/test/ws_test.go`에는 OriginPatterns가 있는 WS server accept path와 기본 cross-origin 거부 path를 직접 검증하는 test가 없다.
- 구현자 검증 출력이 없어 plan command를 실제로 실행했는지 신뢰할 근거가 없다.

### 심볼 참조

- renamed/removed symbol: 없음.
- `proto_socket.NewWsServer`는 기존 signature를 유지해야 한다.
- 새 API 예상 call site: `apps/control-plane/internal/wire/portal.go`.

### 분할 판단

분할 정책을 평가했다. Required는 같은 browser-origin WS handshake를 둘러싼 API 확장, 소비자 적용, regression test, 구현 완료 기록이다. API와 소비자, 테스트가 서로 의존하므로 같은 active task의 단일 follow-up으로 유지한다.

### 범위 결정 근거

- Portal 화면, Dart browser transport, 인증/권한, Control Plane 운영 API는 범위 밖이다.
- sibling `../proto-socket/dart/**`의 기존 dirty changes는 이 계획의 대상이 아니다.
- `PortalHelloRequest`/`PortalHelloResponse` proto 계약은 변경하지 않는다.

### 빌드 등급

Build lane `cloud-G08`, review lane `cloud-G08`. 프로토콜 handshake와 sibling Go library API를 함께 변경하고, 이전 follow-up이 완료되지 않아 검증 신뢰 회복이 필요하다.

## 구현 체크리스트

- [ ] [REVIEW_REVIEW_CP_WS-1] `proto-socket` WS server에 non-breaking accept options API를 추가한다.
- [ ] [REVIEW_REVIEW_CP_WS-2] Control Plane Portal WS endpoint에 제한된 local OriginPatterns를 적용한다.
- [ ] [REVIEW_REVIEW_CP_WS-3] browser-origin regression tests를 추가하고 구현 완료 기록을 채운다.
- [ ] 중간 검증과 최종 검증 명령을 실행하고 실제 출력을 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REVIEW_REVIEW_CP_WS-1] proto-socket accept options

문제: `../proto-socket/go/ws_server.go:130`은 `websocket.Accept(w, r, nil)`로 고정되어 소비자가 OriginPatterns를 전달할 수 없다. 기존 `NewWsServer` call site를 깨지 않고 accept options 주입 경로가 필요하다.

해결 방법: `WsServerOptions`와 `NewWsServerWithOptions`를 추가하고, 기존 `NewWsServer`/`NewWsServerTLS`는 options 없는 wrapper로 유지한다. `WsServer`가 `*websocket.AcceptOptions`를 보관하고 `handleWebSocket`에서 `websocket.Accept(w, r, s.acceptOptions)`를 사용한다.

수정 파일 및 체크리스트:

- [ ] `../proto-socket/go/ws_server.go`: `WsServerOptions`와 `acceptOptions` field를 추가한다.
- [ ] `../proto-socket/go/ws_server.go`: 기존 constructors signature를 유지하고 새 options constructor로 위임한다.
- [ ] `../proto-socket/go/ws_server.go`: `handleWebSocket`에서 configured accept options를 전달한다.

테스트 작성: `REVIEW_REVIEW_CP_WS-3`에서 검증한다.

중간 검증:

```bash
cd ../proto-socket/go && go test ./...
```

기대 결과: 기존 Go proto-socket tests가 모두 통과한다.

### [REVIEW_REVIEW_CP_WS-2] Control Plane local OriginPatterns

문제: `apps/control-plane/internal/wire/portal.go:48`이 options 없는 `proto_socket.NewWsServer`를 계속 사용해 Flutter Web dev Origin이 거부된다.

해결 방법: `proto_socket.NewWsServerWithOptions`를 사용하고 `websocket.AcceptOptions{OriginPatterns: []string{"localhost", "localhost:*", "127.0.0.1", "127.0.0.1:*"}}`를 전달한다. broad `InsecureSkipVerify`는 쓰지 않는다.

수정 파일 및 체크리스트:

- [ ] `apps/control-plane/internal/wire/portal.go`: 제한된 local OriginPatterns를 적용한다.
- [ ] `apps/control-plane/internal/wire/wire.go`: stale reserved/TODO 주석을 현재 WS server 상태에 맞춰 정리한다.

테스트 작성: `REVIEW_REVIEW_CP_WS-3`에서 Control Plane endpoint test로 검증한다.

중간 검증:

```bash
cd /config/workspace/iop && go test -count=1 ./apps/control-plane/internal/wire
```

기대 결과: control-plane wire package tests가 통과한다.

### [REVIEW_REVIEW_CP_WS-3] Regression tests and implementation record

문제: `apps/control-plane/internal/wire/portal_test.go:27`은 Origin 없는 Go client만 검증한다. active `CODE_REVIEW`도 구현 항목 완료 여부, 구현 체크리스트, 검증 출력이 비어 있으면 review skill에서 FAIL 처리된다.

해결 방법: proto-socket Go tests에 default cross-origin reject와 configured OriginPatterns allow test를 추가한다. Control Plane wire test는 `websocket.Dial`에 `DialOptions.HTTPHeader.Set("Origin", "http://localhost:3000")`을 넣어 접속하고 `proto_socket.NewWsClient`로 감싼 뒤 `PortalHelloResponse`까지 검증한다. 구현 완료 후 active `CODE_REVIEW-cloud-G08.md`의 구현자 소유 섹션을 모두 실제 내용으로 채운다.

수정 파일 및 체크리스트:

- [ ] `../proto-socket/go/test/ws_test.go`: default cross-origin reject test와 configured OriginPatterns allow test를 추가한다.
- [ ] `apps/control-plane/internal/wire/portal_test.go`: browser-origin `PortalHello` handshake test를 추가한다.
- [ ] `agent-task/flutter_first_portal_migration/04_control_plane_ws_endpoint/CODE_REVIEW-cloud-G08.md`: 완료 표, 구현 체크리스트, 변경 사항, 실제 검증 stdout/stderr를 채운다.

테스트 작성: 작성한다. 두 테스트 모두 `Origin: http://localhost:3000`을 명시적으로 사용한다.

중간 검증:

```bash
cd ../proto-socket/go && go test ./...
cd /config/workspace/iop && go test -count=1 ./apps/control-plane/internal/wire
```

기대 결과: browser-origin 관련 regression tests가 실패 없이 통과한다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `../proto-socket/go/ws_server.go` | REVIEW_REVIEW_CP_WS-1 |
| `apps/control-plane/internal/wire/portal.go` | REVIEW_REVIEW_CP_WS-2 |
| `apps/control-plane/internal/wire/wire.go` | REVIEW_REVIEW_CP_WS-2 |
| `../proto-socket/go/test/ws_test.go` | REVIEW_REVIEW_CP_WS-3 |
| `apps/control-plane/internal/wire/portal_test.go` | REVIEW_REVIEW_CP_WS-3 |
| `agent-task/flutter_first_portal_migration/04_control_plane_ws_endpoint/CODE_REVIEW-cloud-G08.md` | REVIEW_REVIEW_CP_WS-3 |

## 최종 검증

```bash
cd ../proto-socket/go && go test ./...
cd /config/workspace/iop && go test -count=1 ./apps/control-plane/internal/wire
go test -count=1 ./apps/control-plane/...
go test ./...
git diff --check
```

기대 결과: proto-socket Go tests, control-plane tests, 전체 Go tests, whitespace 검증이 모두 통과한다. `go test ./...`는 최종 전체 회귀 확인으로 cache output을 허용한다.

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