<!-- task=m-agent-message-protocol/02+01_core_compatibility plan=0 tag=API -->

# Plan - API

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

`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채우는 것이 구현의 마지막 필수 단계다. 구현 후 검증을 실행하고 실제 내용과 출력을 리뷰 파일에 남긴 뒤 active 파일을 그대로 둔 채 리뷰 준비를 보고한다. 사용자만 결정할 수 있는 범위 변경, 사용자 소유 외부 환경/secret, 또는 scope conflict가 있으면 리뷰 파일의 `사용자 리뷰 요청`에 근거를 채우고 멈춘다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 증거 공백은 사용자 리뷰 요청이 아니다.

## 배경

`01_proto_contract`가 runner protocol schema와 공통 실패 응답을 정의한 뒤, Core registry와 Dart runner client가 같은 version/capability 협상 기준을 적용해야 한다. 현재 Core는 protocol version이 비어 있지 않은지만 확인하고 capability는 optional로 저장한다. 이 작업은 registration acceptance boundary를 명확히 하면서 기존 runner registration, heartbeat, job report smoke를 유지한다.

## 사용자 리뷰 요청 흐름

구현 중 사용자 결정이 필요한 차단은 active review stub의 `사용자 리뷰 요청` 섹션에 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 형식으로 기록한다. 직접 사용자 프롬프트는 금지이며, code-review가 요청의 타당성을 검증하고 실제 `USER_REVIEW.md` 작성을 소유한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/message-based-build-agent/milestones/agent-message-protocol.md`
- Task ids:
  - `version-capability`: 프로토콜 버전과 capability 협상 기준이 정의되어 있다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-ops/skills/common/router.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/skills/common/update-roadmap/SKILL.md`
- `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/message-based-build-agent/PHASE.md`
- `agent-roadmap/phase/message-based-build-agent/milestones/agent-message-protocol.md`
- `agent-test/local/rules.md`
- `agent-test/local/agent-smoke.md`
- `agent-test/local/core-smoke.md`
- `agent-ops/rules/project/domain/agent/rules.md`
- `Makefile`
- `proto/oto/runner.proto`
- `apps/runner/lib/oto/agent/edge_registration_client.dart`
- `apps/runner/lib/oto/agent/oto_server_job_client.dart`
- `apps/runner/lib/oto/agent/agent_config.dart`
- `apps/runner/lib/oto/agent/agent_runner.dart`
- `apps/runner/test/oto_agent_registration_test.dart`
- `apps/runner/test/oto_agent_migration_plan_test.dart`
- `apps/runner/test/oto_server_connection_smoke_test.dart`
- `apps/runner/pubspec.yaml`
- `services/core/go.mod`
- `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`
- `services/core/internal/cicdstate/store.go`
- `services/core/internal/cicdstate/store_test.go`

### 테스트 환경 규칙

- 선택한 test_env: `local`.
- `agent-test/local/rules.md`는 존재하고 읽었다.
- matching profile: agent 도메인 변경이므로 `agent-test/local/agent-smoke.md`를 적용한다.
- 적용 명령: `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_server_connection_smoke_test.dart`.
- fallback 검증: `services/core/**` 라우팅이 없으므로 `Makefile`과 `services/core/go.mod` 기준 `cd services/core && go test ./...`를 사용한다.
- 공백: local test rules에는 Core service profile이 아직 없다. 이 plan은 fallback을 명시하고 core/proto test profile 보강은 별도 test-rule maintenance 후보로 남긴다.

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

- unsupported protocol rejection: 기존 `registry_test.go`는 missing protocol만 검증한다. unsupported version과 reject code/message test를 추가한다.
- missing/wrong capability rejection: 기존 registry는 capability optional이라 테스트가 없다. name/version/command catalog boundary test를 추가한다.
- HTTP registration response: `server_test.go:99`는 accept path와 missing token만 본다. unsupported protocol/capability JSON response와 기존 200 response shape를 검증한다.
- Dart runner constants: `oto_agent_registration_test.dart:97`은 request 구성만 검증한다. Core가 요구하는 exact constants와 response error fallback test를 추가한다.

### 심볼 참조

- renamed/removed symbols: none.
- `otoRunnerProtocolVersion`, `otoRunnerCapabilityName`, `otoRunnerCapabilityVersion`은 `apps/runner/lib/oto/agent/edge_registration_client.dart:14`에서 정의되고 `apps/runner/test/oto_agent_registration_test.dart:108`에서 검증된다.
- Core registry registration path는 `services/core/internal/runnerregistry/registry.go:56`에서 시작한다. HTTP handler는 `services/core/internal/httpserver/server.go:91`에서 registry response를 그대로 JSON으로 쓴다.

### 분할 판단

- split decision policy를 plan 파일 선택 전에 평가했다.
- shared task group: `agent-task/m-agent-message-protocol/`.
- predecessor `01`: `agent-task/m-agent-message-protocol/01_proto_contract/complete.log` 필요. 작성 시점에는 아직 active predecessor plan만 있고 `complete.log`는 없다.
- current subtask: `02+01_core_compatibility`, `01_proto_contract` 완료 후 구현해야 한다.
- 이 plan은 schema가 확정된 뒤 Core registry, HTTP response, Dart runner request constants를 맞추는 rollout 단계다.

### 범위 결정 근거

- 포함: Core registry acceptance policy, HTTP register response, Dart runner registration constants/tests, Go/Dart smoke tests.
- 제외: run execution lifecycle, cancel/status/self-update endpoint behavior, artifact storage policy는 후속 `remote-run-lifecycle` 마일스톤 범위다.
- 제외: protocol field number 변경이나 generated protobuf 직접 수동 수정은 `01_proto_contract` 범위다.

### 빌드 등급

- build lane/grade: `cloud-G06`. protocol/capability 협상은 cross-language contract와 server acceptance behavior를 바꾸므로 schema predecessor 완료 확인과 Go/Dart smoke evidence가 필요하다.

## 구현 체크리스트

- [ ] `agent-task/m-agent-message-protocol/01_proto_contract/complete.log` 또는 matching archive complete.log가 있는지 확인하고, 없으면 구현하지 않고 review stub의 사용자 리뷰 요청이 아니라 dependency 대기 상태로 기록한다.
- [ ] Core registry에 supported protocol/capability 정책을 추가하고 reject reason과 structured error를 함께 채운다.
- [ ] HTTP registration handler와 tests가 새 reject response를 JSON으로 보존하는지 확인한다.
- [ ] Dart runner registration constants와 JSON payload tests를 Core policy와 맞춘다.
- [ ] 최종 검증 명령을 실행하고 실제 stdout/stderr를 review stub에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

이 task directory 이름은 `02+01_core_compatibility`다. runtime dependency source of truth는 predecessor index `01`이며, `agent-task/m-agent-message-protocol/01_proto_contract/complete.log` 또는 `agent-task/archive/*/*/m-agent-message-protocol/01_proto_contract/complete.log`가 있어야 구현을 시작한다. 작성 시점에는 predecessor complete evidence가 없다.

### [API-1] Core registry compatibility policy

#### 문제

`services/core/internal/runnerregistry/registry.go:61`은 enrollment token, runner id, protocol version 공백만 검증한다. `services/core/internal/runnerregistry/registry.go:82`는 capability가 있으면 저장하지만 없거나 다른 capability여도 registration을 accept한다. 이 상태에서는 runner와 server가 같은 protocol/capability 기준으로 병렬 구현을 시작했다는 증거가 부족하다.

#### 해결 방법

새 helper를 registry 내부에 둔다. 새 dependency를 추가하지 않고 stdlib 문자열 처리로 exact protocol과 capability name, compatible capability major version을 판정한다.

```go
// services/core/internal/runnerregistry/registry.go:61 before
protocolVersion := strings.TrimSpace(req.GetProtocolVersion())
if token == "" {
	return rejected("missing enrollment token")
}
```

```go
// after
protocolVersion := strings.TrimSpace(req.GetProtocolVersion())
if token == "" {
	return rejected("missing enrollment token")
}
if err := validateProtocolCompatibility(protocolVersion, req.GetCapability()); err != nil {
	return rejectedWithCode("incompatible_runner", err.Error())
}
```

Policy:
- supported protocol: `oto.runner.v1`.
- required capability name: `oto-runner`.
- capability version: same major version as Core supported runner capability; reject empty or incompatible major.
- command catalog may be empty for bootstrap smoke but, when present, it remains the command extension boundary.

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

- [ ] `services/core/internal/runnerregistry/registry.go`: constants and validation helper 추가.
- [ ] `services/core/internal/runnerregistry/registry_test.go`: unsupported protocol, missing capability, wrong capability, compatible capability tests 추가.
- [ ] `services/core/oto/runner.pb.go`: predecessor generated code와 compile 호환 확인만 한다.

#### 테스트 작성

`services/core/internal/runnerregistry/registry_test.go`에 table-driven rejection tests를 추가한다. 기존 `TestRegistryRejectsIncompleteRegistration`과 합치거나 새 `TestRegistryRejectsIncompatibleRunnerContract`로 분리한다.

#### 중간 검증

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

### [API-2] HTTP and Dart registration contract

#### 문제

`services/core/internal/httpserver/server.go:91`의 register handler는 registry response를 그대로 `200`으로 반환한다. `apps/runner/lib/oto/agent/edge_registration_client.dart:14`의 Dart constants가 Core policy와 같은 값이라는 검증은 request construction 수준에 머문다. `apps/runner/test/oto_agent_registration_test.dart:158`은 protocol/capability payload 일부만 확인한다.

#### 해결 방법

Core reject response는 기존 `accepted=false` shape를 유지하고, `reject_reason`과 predecessor schema의 `error` object를 함께 채운다. Dart request test는 protocol/capability version까지 exact match를 검증하고, rejected response parser는 `reject_reason` fallback을 유지한다.

```dart
// apps/runner/lib/oto/agent/edge_registration_client.dart:14 before
const otoRunnerProtocolVersion = 'oto.runner.v1';
const otoRunnerCapabilityName = 'oto-runner';
const otoRunnerCapabilityVersion = '1.0.0';
```

```dart
// after
const otoRunnerProtocolVersion = 'oto.runner.v1';
const otoRunnerCapabilityName = 'oto-runner';
const otoRunnerCapabilityVersion = '1.0.0';
```

상수 값은 바꾸지 않는다. 테스트에서 Core policy와 같은 exact values를 검증한다.

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

- [ ] `services/core/internal/httpserver/server.go`: register reject response가 structured error를 보존하는지 확인하고 필요한 mapping을 추가한다.
- [ ] `services/core/internal/httpserver/server_test.go`: unsupported protocol/capability JSON response test 추가.
- [ ] `apps/runner/lib/oto/agent/edge_registration_client.dart`: constants 유지 또는 helper 추가.
- [ ] `apps/runner/test/oto_agent_registration_test.dart`: protocol/capability version payload와 rejected response parsing assertion 추가.
- [ ] `apps/runner/test/oto_server_connection_smoke_test.dart`: 기존 accepted smoke가 계속 통과하는지 확인한다.

#### 테스트 작성

Go: `TestHandleRunnerRegisterRejectsIncompatibleProtocol`와 `TestHandleRunnerRegisterRejectsIncompatibleCapability` 추가. Dart: existing `builds OTO Server registration request from config`와 `posts registration request` assertion에 capability version을 포함하고 rejected response parser test를 추가한다.

#### 중간 검증

```bash
cd services/core && go test ./internal/httpserver ./internal/runnerregistry
cd apps/runner && dart test test/oto_agent_registration_test.dart
```

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `services/core/internal/runnerregistry/registry.go` | API-1 |
| `services/core/internal/runnerregistry/registry_test.go` | API-1 |
| `services/core/internal/httpserver/server.go` | API-2 |
| `services/core/internal/httpserver/server_test.go` | API-2 |
| `apps/runner/lib/oto/agent/edge_registration_client.dart` | API-2 |
| `apps/runner/test/oto_agent_registration_test.dart` | API-2 |
| `apps/runner/test/oto_server_connection_smoke_test.dart` | API-2 검증 |

## 최종 검증

```bash
test -f agent-task/m-agent-message-protocol/01_proto_contract/complete.log || find agent-task/archive -path '*/m-agent-message-protocol/01_proto_contract/complete.log' -print -quit | grep -q .
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_server_connection_smoke_test.dart
```

기대 결과: predecessor complete evidence가 존재하고, Go Core 전체 테스트와 agent local smoke baseline이 통과한다. 모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
