<!-- task=oto_agent_registration/02+01_edge_registration_runtime plan=0 tag=AGENT_REG -->

# Plan - AGENT_REG

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

구현 완료 전 `CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션을 반드시 채운다. 검증 명령을 실제로 실행하고 출력 원문을 붙이며, active 파일은 그대로 둔 채 review 준비 상태를 보고한다. `complete.log` 작성, `.log` 아카이브, task 디렉터리 이동은 code-review-skill 전용이다.

## 배경

`01_agent_cli_config`가 `oto agent run --config`와 config parser를 만든 뒤에는 실제 Edge outbound registration runtime이 필요하다. 기존 `test/oto_iop_connection_smoke_test.dart`는 ad hoc client로 iop Edge `RegisterRequest/RegisterResponse` smoke를 통과하지만 production runner는 없다. 이 작업은 bootstrap config를 소비해 Edge에 직접 등록하고 registration result를 상태로 구분하는 최소 runtime을 만든다.

## 분석 결과

### 읽은 파일

- `agent-ops/roadmap/current.md`
- `agent-ops/roadmap/milestones/edge-bootstrap-contract.md`
- `agent-ops/roadmap/milestones/oto-agent-registration.md`
- `agent-ops/rules/project/domain/cli/rules.md`
- `agent-ops/rules/project/domain/core/rules.md`
- `bin/main.dart`
- `lib/cli/commands/command_base.dart`
- `lib/cli/commands/command_manager.dart`
- `lib/cli/commands/command_validate.dart`
- `lib/cli/commands/command_exe.dart`
- `pubspec.yaml`
- `assets/script/shell/oto_agent_bootstrap.sh`
- `test/oto_agent_bootstrap_script_test.dart`
- `test/oto_validate_cli_test.dart`
- `test/oto_catalog_cli_test.dart`
- `test/oto_iop_connection_smoke_test.dart`

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

- production `AgentRunner` 없음: 새 unit test가 필요하다.
- config token에서 `RegisterRequest(token)`으로 이어지는 mapping 없음: 새 unit test가 필요하다.
- command-level Edge smoke 없음: 기존 smoke는 test-local client만 사용한다. 새 또는 갱신 smoke가 필요하다.
- first heartbeat 기반 online 판정은 iop Edge 지원 상태에 따라 구현 가능 범위를 확인해야 한다. 지원이 부족하면 accepted registration과 heartbeat send attempt를 구분해 기록한다.

### 심볼 참조

- renamed/removed symbol: 없음.
- `test/oto_iop_connection_smoke_test.dart:14`의 ad hoc `_OtoIopSmokeClient`는 production client 도입 후 테스트 중복 여부를 판단한다.

### 분할 판단

- split decision policy를 먼저 평가했다.
- shared task group: `oto_agent_registration`
- 현재 subtask: `02+01_edge_registration_runtime`
- dependency: `01_agent_cli_config`가 `complete.log`를 만든 뒤 시작한다.
- 분할 이유: protocol/socket/heartbeat와 command runner integration은 CLI/config scaffold와 위험도가 달라 별도 cloud-G07 계획으로 분리한다.

### 범위 결정 근거

- 포함: Edge 연결, token 기반 RegisterRequest, accepted/rejected 상태, command runner wiring, focused tests.
- 제외: YAML remote run, cancel/status/self-update, artifact/log stream, checksum/signature/fingerprint 강제 구현, 장기 credential rotation.
- iop Edge registry/API 구현은 OTO repo 범위가 아니다.
- bootstrap shell script는 이 작업에서 수정하지 않는다.

### 빌드 등급

- build=`cloud-G07`, review=`cloud-G07`.
- 근거: proto-socket integration, process/network smoke, long-running agent runtime 경계가 있어 terminal/protocol risk가 있다.

## 구현 체크리스트

- [ ] AGENT_REG-1 production iop runtime message import 경계를 만든다.
- [ ] AGENT_REG-2 Edge registration client와 registration result 모델을 추가한다.
- [ ] AGENT_REG-3 `CommandAgent` default runner를 registration runtime에 연결한다.
- [ ] AGENT_REG-4 registration unit/smoke 테스트를 추가하거나 기존 smoke를 production client 기준으로 갱신한다.
- [ ] 모든 중간 검증과 최종 검증을 실행하고 실제 stdout/stderr를 `CODE_REVIEW-cloud-G07.md`에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

- 이 subtask directory 이름 `02+01_edge_registration_runtime`의 `+01`이 런타임 의존성이다.
- 구현 시작 전 `agent-task/oto_agent_registration/01_agent_cli_config/complete.log`가 있어야 한다.

### [AGENT_REG-1] production iop runtime message import 경계

#### 문제

- `test/oto_iop_connection_smoke_test.dart:7`은 `test/support/iop/runtime.pb.dart`를 import한다.
- production code에서 `RegisterRequest`와 `RegisterResponse`를 쓰려면 test support가 아닌 production import 경계가 필요하다.
- `pubspec.yaml:30-32`에서 `fixnum`과 `protobuf`가 dev dependency라 production generated protobuf code가 직접 의존하기 어렵다.

#### 해결 방법

- production generated protobuf files를 `lib/oto/agent/iop/` 아래에 둔다.
- 초기 구현은 test support generated files와 같은 iop runtime proto version을 사용한다.
- `fixnum`과 `protobuf`를 regular dependencies로 이동하고, test support와 production code가 같은 dependency를 사용하게 한다.
- generated files는 직접 hand-edit하지 않는다. 필요하면 `../iop/proto/iop/runtime.proto` 기준으로 재생성하고 재생성 명령을 `계획 대비 변경 사항`에 기록한다.

Before:

```yaml
# pubspec.yaml:30-32
dev_dependencies:
  fixnum: ^1.1.1
  protobuf: ^3.1.0
```

After:

```yaml
dependencies:
  fixnum: ^1.1.1
  protobuf: ^3.1.0
  proto_socket:
    path: ../proto-socket/dart

dev_dependencies:
  lints: ^6.0.0
```

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

- [ ] `pubspec.yaml`: `fixnum`, `protobuf`를 regular dependencies로 이동.
- [ ] `lib/oto/agent/iop/runtime.pb.dart`: production generated message 추가.
- [ ] `lib/oto/agent/iop/runtime.pbenum.dart`: production generated enum 추가.
- [ ] `lib/oto/agent/iop/runtime.pbjson.dart`: production generated json descriptor 추가.
- [ ] `lib/oto/agent/iop/runtime.pbserver.dart`: 필요한 경우 production generated server stub 추가.

#### 테스트 작성

- 작성: 직접 테스트보다 AGENT_REG-2/4 테스트가 import 성공과 runtime message 사용을 검증한다.

#### 중간 검증

```bash
dart pub get
dart analyze
```

예상 결과: dependency resolution 성공, analyzer 오류 없음.

### [AGENT_REG-2] Edge registration client와 result model

#### 문제

- 기존 smoke는 `test/oto_iop_connection_smoke_test.dart:65`에서 직접 `ProtobufClient.connect`를 호출하고 `RegisterRequest`를 보낸다.
- production code에는 config의 `edge.url`과 `agent.enrollment_token`을 사용해 등록하는 runtime client가 없다.

#### 해결 방법

- `lib/oto/agent/edge_registration_client.dart`를 추가한다.
- `EdgeRegistrationClient.register(AgentConfig config)`는 `edge.url`에서 host/port를 파싱하고 `RegisterRequest(token)`을 보낸다.
- accepted false면 `RegistrationResult.rejected(reason)`을 반환한다.
- accepted true면 `RegistrationResult.accepted(nodeId, alias, runtimeConfig)`를 반환한다.
- socket close는 항상 finally에서 처리한다.

새 파일 핵심 형태:

```dart
class EdgeRegistrationClient {
  Future<RegistrationResult> register(AgentConfig config) async {
    final endpoint = EdgeEndpoint.parse(config.edge.url);
    final socket = await ProtobufClient.connect(endpoint.host, endpoint.port);
    final client = _OtoIopClient(socket);
    try {
      final response =
          await client.sendRequest<iop.RegisterRequest, iop.RegisterResponse>(
        iop.RegisterRequest()..token = config.agent.enrollmentToken,
        timeout: const Duration(seconds: 5),
      );
      return RegistrationResult.fromResponse(response);
    } finally {
      await client.close();
    }
  }
}
```

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

- [ ] `lib/oto/agent/edge_registration_client.dart`: client, endpoint parser, result model 추가.
- [ ] `lib/oto/agent/agent_runner.dart`: runner가 client를 주입받을 수 있는 구조 추가.

#### 테스트 작성

- 작성: `test/oto_agent_registration_test.dart`
- 테스트 이름:
  - `EdgeEndpoint parses host and port from config edge url`
  - `AgentRunner maps config token to register request`
  - `AgentRunner reports rejected registration`
- 검증 목표: network 없이 fake client로 mapping/state를 검증한다.

#### 중간 검증

```bash
dart test test/oto_agent_registration_test.dart
```

예상 결과: 모든 테스트 통과.

### [AGENT_REG-3] CommandAgent default runner 연결

#### 문제

- `01_agent_cli_config`는 CLI parsing과 config loading까지만 만든다.
- bootstrap script가 실행하는 `oto agent run --config "$config_path"`는 실제 runner와 연결되어야 한다.

#### 해결 방법

- `CommandAgent()` default 생성자에서 `AgentRunner(EdgeRegistrationClient())`를 사용한다.
- `agent run --config`는 등록 결과에 따라 exit code를 설정한다.
- accepted면 등록 성공 메시지와 node/alias를 출력한다.
- rejected 또는 connection failure면 stderr/help가 아니라 명확한 error message와 exit code 10을 사용한다.
- 장기 daemon loop, retry, self-update는 제외한다.

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

- [ ] `lib/cli/commands/command_agent.dart`: default runner 연결.
- [ ] `lib/oto/agent/agent_runner.dart`: runner result를 command exit code로 변환 가능한 형태로 제공.

#### 테스트 작성

- 작성: `test/oto_agent_cli_test.dart` 갱신.
- 테스트 이름:
  - `CommandAgent sets exit code zero on accepted registration`
  - `CommandAgent sets exit code ten on rejected registration`
- 검증 목표: command가 runner result를 exit code와 output으로 반영한다.

#### 중간 검증

```bash
dart test test/oto_agent_cli_test.dart test/oto_agent_registration_test.dart
```

예상 결과: 모든 테스트 통과.

### [AGENT_REG-4] Production client 기준 smoke

#### 문제

- `test/oto_iop_connection_smoke_test.dart:14`의 `_OtoIopSmokeClient`는 test-local smoke client다.
- production client가 iop Edge registration smoke를 통과한다는 근거가 없다.

#### 해결 방법

- 기존 smoke를 production `EdgeRegistrationClient` 또는 `AgentRunner` 기준으로 바꾼다.
- temp config는 bootstrap config shape와 같게 만든다.
- iop Edge 실행은 기존 `go run ./apps/edge/cmd/edge serve --config` 방식을 유지한다.
- heartbeat는 iop Edge 지원 여부를 먼저 확인한다. 지원이 없다면 이 smoke는 registration accepted까지만 검증하고, heartbeat/online은 roadmap 확인 필요 또는 별도 follow-up으로 남긴다.

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

- [ ] `test/oto_iop_connection_smoke_test.dart`: production client/runner 사용으로 갱신.
- [ ] `test/oto_agent_registration_test.dart`: accepted/rejected unit coverage 보강.

#### 테스트 작성

- 작성/갱신: `test/oto_iop_connection_smoke_test.dart`
- 검증 목표: OTO production registration runtime이 iop Edge `RegisterRequest/RegisterResponse` 경로를 통과한다.

#### 중간 검증

```bash
dart test test/oto_iop_connection_smoke_test.dart
```

예상 결과: iop Edge registration smoke 통과.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `pubspec.yaml` | AGENT_REG-1 |
| `lib/oto/agent/iop/runtime.pb.dart` | AGENT_REG-1 |
| `lib/oto/agent/iop/runtime.pbenum.dart` | AGENT_REG-1 |
| `lib/oto/agent/iop/runtime.pbjson.dart` | AGENT_REG-1 |
| `lib/oto/agent/iop/runtime.pbserver.dart` | AGENT_REG-1 |
| `lib/oto/agent/edge_registration_client.dart` | AGENT_REG-2 |
| `lib/oto/agent/agent_runner.dart` | AGENT_REG-2, AGENT_REG-3 |
| `lib/cli/commands/command_agent.dart` | AGENT_REG-3 |
| `test/oto_agent_registration_test.dart` | AGENT_REG-2, AGENT_REG-3 |
| `test/oto_agent_cli_test.dart` | AGENT_REG-3 |
| `test/oto_iop_connection_smoke_test.dart` | AGENT_REG-4 |

## 최종 검증

```bash
dart pub get
dart analyze
dart test test/oto_agent_config_test.dart test/oto_agent_cli_test.dart test/oto_agent_registration_test.dart
dart test test/oto_iop_connection_smoke_test.dart
```

예상 결과: dependency resolution 성공, analyzer 오류 없음, agent unit tests와 iop registration smoke 통과.

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