<!-- task=m-control-plane-separation-migration/02+01_runner_registration plan=0 tag=API -->

# Plan - API

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

`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채우는 것이 구현의 마지막 단계다. 구현 후 검증을 실행하고 실제 출력과 변경 내용을 review stub에 남긴 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 사용자 결정, 사용자 소유 외부 환경, scope 충돌로 막히면 review stub의 `사용자 리뷰 요청` 섹션에 근거를 채우고 멈춘다. 직접 사용자에게 묻거나 `USER_REVIEW.md`, `complete.log`, archive log를 만들지 않는다.

## 배경

runner 등록은 현재 iop Edge token만 보내고 OTO Server registry에 아무 상태도 남기지 않는다. Server-Runner 계약은 enrollment token과 함께 runner id, alias, protocol version, command catalog summary를 전달해야 한다. `01_wire_contract`가 완료된 뒤 generated OTO proto를 사용해 Go server와 Dart runner의 등록 표면을 연결한다.

## 사용자 리뷰 요청 흐름

구현 중 차단은 active `CODE_REVIEW-*-G??.md`의 `사용자 리뷰 요청` 섹션에 기록한다. 직접 사용자 프롬프트는 금지되며, code-review가 요청의 타당성을 검증하고 실제 `USER_REVIEW.md` 작성을 소유한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/independent-control-plane/milestones/control-plane-separation-migration.md`
- Task ids:
  - `runner-register`: runner 등록 요청/응답은 enrollment token뿐 아니라 runner id, alias, protocol version, command catalog/capability summary를 전달할 수 있게 정의된다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-test/local/rules.md`
- `agent-test/local/agent-smoke.md`
- `agent-ops/rules/project/domain/agent/rules.md`
- `services/core/internal/httpserver/server.go`
- `services/core/internal/httpserver/server_test.go`
- `services/core/cmd/oto-core/main.go`
- `services/core/go.mod`
- `apps/runner/lib/oto/agent/agent_config.dart`
- `apps/runner/lib/oto/agent/agent_runner.dart`
- `apps/runner/lib/oto/agent/edge_registration_client.dart`
- `apps/runner/lib/cli/commands/command_agent.dart`
- `apps/runner/test/oto_agent_config_test.dart`
- `apps/runner/test/oto_agent_registration_test.dart`
- `apps/runner/test/oto_agent_cli_test.dart`
- `apps/runner/test/oto_agent_migration_plan_test.dart`
- `apps/runner/pubspec.yaml`

### 테스트 환경 규칙

- `test_env=local`.
- agent profile 적용: `cd apps/runner && dart analyze`, `cd apps/runner && dart test test/oto_agent_migration_plan_test.dart test/oto_agent_bootstrap_script_test.dart test/oto_agent_cli_test.dart test/oto_agent_config_test.dart test/oto_agent_registration_test.dart test/oto_iop_connection_smoke_test.dart`.
- core service는 profile이 없어 fallback `cd services/core && go test ./...`를 사용한다.
- `02+01`은 predecessor `01`의 `complete.log`가 필요하다. 작성 시점에는 `01_wire_contract` active plan/review만 있고 `complete.log`가 없으므로, 구현 시작 전 완료 근거를 다시 확인해야 한다.

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

- OTO Server registration handler: 기존 테스트 없음. Go handler/registry tests 추가 필요.
- Dart runner registration request payload: 기존 test는 token 전달만 확인한다. protocol version/catalog/capabilities assertion 추가 필요.
- Loopback OTO Server registration smoke: migration plan test가 future boundary만 담고 실제 fixture는 없음. 이번 작업에서 focused integration test를 추가한다.

### 심볼 참조

- `EdgeRegistrationClient`: `apps/runner/lib/oto/agent/agent_runner.dart`, `apps/runner/test/oto_agent_registration_test.dart`, `apps/runner/test/oto_iop_connection_smoke_test.dart`.
- `EdgeEndpoint`: `apps/runner/test/oto_agent_registration_test.dart`.
- `EdgeConnectionConfig`: `apps/runner/lib/oto/agent/agent_config.dart`, `apps/runner/test/oto_agent_config_test.dart`, `apps/runner/test/oto_agent_registration_test.dart`, `apps/runner/test/oto_agent_cli_test.dart`, `apps/runner/test/oto_iop_connection_smoke_test.dart`.
- Rename는 이번 작업에서 공개 config 호환을 깨지 않도록 optional alias class 또는 compatibility parser를 유지한다.

### 분할 판단

- split decision policy를 평가했고, API foundation과 registration rollout 경계가 있어 분할한다.
- 이 plan은 `02+01_runner_registration`이며 predecessor `01_wire_contract` 완료가 필요하다.
- 작성 시점 predecessor 상태: missing complete log. `agent-task/m-control-plane-separation-migration/01_wire_contract/`에는 active plan/review만 있다.
- 구현 전 `agent-task/m-control-plane-separation-migration/01_wire_contract/complete.log` 또는 archive의 matching `01_*` complete log를 확인한다.

### 범위 결정 근거

- heartbeat timeout과 disconnect 상태는 `03+02_online_state`로 분리한다.
- bootstrap command endpoint는 `04+02_bootstrap_command`로 분리한다.
- iop smoke는 migration evidence로 유지하고 이번 작업에서 삭제하지 않는다.
- `apps/client`는 아직 등록 API 소비자가 아니므로 제외한다.

### 빌드 등급

- `cloud-G06`: protocol payload와 Go/Dart call-site migration이 함께 있어 cloud lane이 적합하다.

## 구현 체크리스트

- [x] [API-1] predecessor `01_wire_contract` 완료 근거를 확인한다.
- [x] [API-2] services/core에 in-memory runner registry와 registration handler를 추가한다.
- [x] [API-3] Dart runner registration client가 OTO Server 계약으로 runner id, alias, protocol version, catalog/capability summary를 보낸다.
- [x] [API-4] Go/Dart tests를 추가하고 iop smoke는 migration evidence로 유지한다. 검증: `cd services/core && go test ./...`, `cd apps/runner && dart analyze`, agent test subset 통과.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

- `02+01_runner_registration`는 directory name 기준으로 `01_wire_contract`에 의존한다.
- 구현 시작 전 `agent-task/m-control-plane-separation-migration/01_wire_contract/complete.log` 또는 archive matching `01_*` complete log를 확인한다.

### [API-1] Predecessor 확인

문제: 이 작업은 generated OTO proto를 import해야 하므로 `01_wire_contract` 없이 구현하면 파일 경로와 message 이름을 추정하게 된다.

해결 방법: 구현 전 predecessor complete log를 확인하고, 없으면 review stub의 사용자 리뷰 요청이 아니라 작업 미충족으로 중단한다.

수정 파일 및 체크리스트:

- [ ] active predecessor complete log 확인
- [ ] 없으면 archive matching candidate 확인

테스트 작성: 없음.

중간 검증:

```bash
find agent-task/m-control-plane-separation-migration -path '*/01_*/*' -name complete.log -print
```

### [API-2] Core registration handler

문제: [services/core/internal/httpserver/server.go](/config/workspace/oto/services/core/internal/httpserver/server.go:15)는 `/healthz`, `/readyz`만 등록하고 runner registration surface가 없다.

해결 방법: `internal/runnerregistry` 패키지와 `/api/v1/runners/register` handler를 추가한다. handler는 enrollment token, runner id, alias, protocol version, command catalog summary, capabilities를 받아 accepted response와 server-assigned bootstrap/status hints를 반환한다.

Before:

```go
15 func NewServer(addr string) *Server {
16     mux := http.NewServeMux()
19     mux.HandleFunc("/healthz", handleHealthz)
20     mux.HandleFunc("/readyz", handleReadyz)
```

After:

```go
registry := runnerregistry.New()
mux.HandleFunc("/api/v1/runners/register", handleRunnerRegister(registry))
```

수정 파일 및 체크리스트:

- [ ] `services/core/internal/runnerregistry/registry.go`
- [ ] `services/core/internal/runnerregistry/registry_test.go`
- [ ] `services/core/internal/httpserver/server.go`
- [ ] `services/core/internal/httpserver/server_test.go`

테스트 작성: Go tests 추가. accepted registration, missing token reject, registry stores alias/protocol/catalog/capabilities.

중간 검증:

```bash
cd services/core && go test ./...
```

### [API-3] Dart runner registration client

문제: [apps/runner/lib/oto/agent/edge_registration_client.dart](/config/workspace/oto/apps/runner/lib/oto/agent/edge_registration_client.dart:143)는 `iop.RegisterRequest()..token`만 보낸다. [apps/runner/lib/oto/agent/agent_runner.dart](/config/workspace/oto/apps/runner/lib/oto/agent/agent_runner.dart:34)는 user-facing log도 OTO Edge를 말한다.

해결 방법: `OtoServerRegistrationClient`를 추가하거나 기존 class를 compatibility wrapper로 유지하면서 내부 구현을 OTO Server contract로 교체한다. `AgentConfig`에는 `server` section을 새 표준으로 추가하고, 기존 `edge` section은 migration compatibility로 읽는다.

Before:

```dart
final response =
    await client.sendRequest<iop.RegisterRequest, iop.RegisterResponse>(
  iop.RegisterRequest()..token = config.agent.enrollmentToken,
  timeout: const Duration(seconds: 5),
);
```

After:

```dart
final request = RegisterRunnerRequest()
  ..enrollmentToken = config.agent.enrollmentToken
  ..runnerId = config.agent.id
  ..alias = config.agent.alias ?? ''
  ..protocolVersion = 'oto.runner.v1';
```

수정 파일 및 체크리스트:

- [ ] `apps/runner/lib/oto/agent/agent_config.dart`
- [ ] `apps/runner/lib/oto/agent/agent_runner.dart`
- [ ] `apps/runner/lib/oto/agent/edge_registration_client.dart` 또는 새 `oto_server_registration_client.dart`
- [ ] `apps/runner/test/oto_agent_config_test.dart`
- [ ] `apps/runner/test/oto_agent_registration_test.dart`
- [ ] `apps/runner/test/oto_agent_cli_test.dart`

테스트 작성: Dart tests 수정/추가. server config parsing, payload includes runner id/alias/protocol/catalog, rejected response closes session.

중간 검증:

```bash
cd apps/runner && dart test test/oto_agent_config_test.dart test/oto_agent_registration_test.dart test/oto_agent_cli_test.dart
```

### [API-4] Loopback registration smoke

문제: [apps/runner/test/oto_agent_migration_plan_test.dart](/config/workspace/oto/apps/runner/test/oto_agent_migration_plan_test.dart:56)는 target fixture를 future boundary로만 정의한다.

해결 방법: services/core를 dynamic port로 시작하거나 `httptest` equivalent endpoint를 Dart test fixture로 제공해 runner registration accepted evidence를 수집한다. 외부 iop checkout 없이 실행되어야 한다.

수정 파일 및 체크리스트:

- [ ] `apps/runner/test/oto_agent_migration_plan_test.dart` 또는 새 focused test
- [ ] `agent-test/local/agent-smoke.md` 명령이 새 test를 포함하는지 확인

테스트 작성: Dart focused integration test 추가. iop smoke 삭제는 제외.

중간 검증:

```bash
cd apps/runner && dart test test/oto_agent_migration_plan_test.dart test/oto_agent_registration_test.dart
```

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `services/core/internal/runnerregistry/registry.go` | API-2 |
| `services/core/internal/runnerregistry/registry_test.go` | API-2 |
| `services/core/internal/httpserver/server.go` | API-2 |
| `services/core/internal/httpserver/server_test.go` | API-2 |
| `apps/runner/lib/oto/agent/agent_config.dart` | API-3 |
| `apps/runner/lib/oto/agent/agent_runner.dart` | API-3 |
| `apps/runner/lib/oto/agent/edge_registration_client.dart` | API-3 |
| `apps/runner/test/oto_agent_config_test.dart` | API-3 |
| `apps/runner/test/oto_agent_registration_test.dart` | API-3 |
| `apps/runner/test/oto_agent_cli_test.dart` | API-3 |
| `apps/runner/test/oto_agent_migration_plan_test.dart` | API-4 |
| `agent-test/local/agent-smoke.md` | API-4 |

## 최종 검증

```bash
cd services/core && go test ./...
cd apps/runner && dart analyze
cd apps/runner && dart test test/oto_agent_migration_plan_test.dart test/oto_agent_bootstrap_script_test.dart test/oto_agent_cli_test.dart test/oto_agent_config_test.dart test/oto_agent_registration_test.dart test/oto_iop_connection_smoke_test.dart
```

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