<!-- task=m-node-resource-model-unification/01_contract_schema plan=0 tag=API -->

# Node Resource Model Contract Plan

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

이 계획을 구현한 뒤에는 반드시 같은 디렉터리의 `CODE_REVIEW-cloud-G07.md`에서 구현 에이전트 소유 섹션을 실제 구현 내용, 설계 결정, 검증 출력으로 채운다. 구현 중 선택된 Milestone의 `구현 잠금 > 결정 필요`가 실구현을 막을 때만 review stub의 `사용자 리뷰 요청` 섹션을 채우고 멈춘다. 직접 사용자에게 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하지 않는다. 외부 환경, secret, 서비스 준비, 증거 공백은 사용자 리뷰 요청이 아니라 검증/후속 계획으로 기록한다.

## 배경

현재 config와 wire는 `nodes[].adapters`와 `nodes[].providers`를 이미 갖고 있지만, 계약 문서는 `NodeRuntimeConfig.concurrency`를 node-wide 실행 제한으로 정의한다. 사용자가 정리한 방향은 node가 global gate가 아니라 하나의 identity/connection owner이고, provider/CLI 같은 resource가 node 아래에 명시되는 구조다. 첫 단계는 구현 전 계약과 validation 기준을 맞춰 이후 Node/Edge 변경이 같은 의미를 공유하게 하는 것이다.

## 사용자 리뷰 요청 흐름

구현 중 사용자 리뷰 요청은 선택된 Milestone lock 결정이 직접 막을 때만 `CODE_REVIEW-cloud-G07.md`의 `사용자 리뷰 요청` 섹션에 기록한다. 선택된 Milestone의 구현 잠금은 해제되어 있으므로 기본값은 `없음`이다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md`
- Task ids:
  - `contract-schema`: Node runtime contract and resource catalog validation
- 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/_templates/implementation-user-review-request-section.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/operational-observability-provider-management/PHASE.md`
- `agent-roadmap/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md`
- `agent-roadmap/sdd/operational-observability-provider-management/node-resource-model-unification/SDD.md`
- `agent-roadmap/phase/update-plane-self-update-foundation/PHASE.md`
- `agent-roadmap/phase/update-plane-self-update-foundation/milestones/update-plane-stable-protocol.md`
- `agent-test/local/rules.md`
- `agent-test/local/node-smoke.md`
- `agent-test/local/edge-smoke.md`
- `agent-test/local/platform-common-smoke.md`
- `agent-test/dev/inventory.yaml`
- `agent-test/dev/edge-smoke.md`
- `agent-test/dev/node-smoke.md`
- `agent-test/dev/platform-common-smoke.md`
- `agent-contract/index.md`
- `agent-contract/inner/edge-node-runtime-wire.md`
- `agent-contract/inner/control-plane-edge-wire.md`
- `agent-contract/inner/edge-config-runtime-refresh.md`
- `agent-contract/outer/openai-compatible-api.md`
- `proto/iop/runtime.proto`
- `proto/iop/control.proto`
- `packages/go/config/config.go`
- `apps/edge/internal/edgevalidate/validate.go`
- `apps/edge/internal/configrefresh/classify.go`
- `apps/edge/internal/node/mapper.go`
- `apps/node/internal/adapters/config_set.go`
- `packages/go/config/config_test.go`
- `apps/edge/internal/configrefresh/classify_test.go`
- `apps/edge/internal/node/mapper_test.go`
- `apps/node/internal/adapters/config_set_test.go`

### SDD 기준

선택된 Milestone은 `SDD: 필요`이며 `agent-roadmap/sdd/operational-observability-provider-management/node-resource-model-unification/SDD.md`가 `[승인됨]`, SDD 잠금 `해제` 상태다. 이 subtask는 Acceptance Scenario `S01`, `S02`와 Milestone Task `contract-schema`를 대상으로 한다.

Evidence Map은 contract/proto/comment diff, config validation unit test, missing/disabled adapter validation output을 요구한다. 따라서 구현 체크리스트는 `NodeRuntimeConfig.concurrency` legacy/no-op 계약 정리, `nodes[].providers[]` resource catalog 문서화, provider adapter reference validation, 관련 regression test를 포함한다.

### 테스트 환경 규칙

test_env는 기본값 `local`이다. `agent-test/local/rules.md`와 matching profile `node-smoke`, `edge-smoke`, `platform-common-smoke`를 읽었다. 이 subtask는 `packages/go/**`, `proto/**`, `agent-contract/**`를 건드리므로 `go test -count=1 ./packages/go/... ./proto/gen/...`와, proto를 바꾸는 경우 `make proto`를 적용한다. dev-runtime 문서와 원격 smoke는 이 subtask의 필수 검증이 아니며 04번 subtask가 맡는다.

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

- `NodeRuntimeConfig.concurrency` 의미를 legacy/no-op으로 바꾸는 계약 문구는 문서성 변경이라 unit coverage가 없다.
- provider adapter reference validation은 config/edgevalidate regression test가 필요하다.
- `ProviderSnapshot`을 resource snapshot으로 취급하는 compat 의미는 proto field 추가 없이 문서와 status tests로 검증한다.

### 심볼 참조

삭제/rename 예정 symbol은 없다. 의미 변경 대상은 `NodeRuntimeConfig.concurrency`, `ProviderSnapshot`, `nodes[].providers[].adapter`, `runtime.concurrency` 문자열이다.

### 분할 판단

split policy를 먼저 평가했다. 공유 task group은 `m-node-resource-model-unification`이다.

- `01_contract_schema`: 계약/config validation 기준.
- `02+01_node_admission`: 01 완료 후 Node admission 의미 변경.
- `03+01_edge_dispatch_status`: 01 완료 후 Edge dispatch/status 의미 변경.
- `04+01,02,03_dev_runtime_profile`: 앞선 세 구현 완료 후 dev-runtime 문서/config/smoke 정리.

이 subtask는 선행 의존성이 없다.

### 범위 결정 근거

새 외부 API endpoint나 gRPC 도입은 제외한다. `proto`는 가능하면 field 추가 없이 legacy `ProviderSnapshot`을 resource snapshot compat surface로 문서화하고, 실제 새 field가 꼭 필요하다고 구현 중 판단될 때만 `make proto`와 생성물 diff를 포함한다.

### 빌드 등급

`cloud-G07`: config/proto/contract 의미가 여러 런타임 경계에 영향을 주며, wrong-local implementation이 dev/runtime status를 깨뜨릴 수 있다.

## 구현 체크리스트

- [ ] `NodeRuntimeConfig.concurrency`를 node-wide admission source가 아닌 legacy/compat runtime payload로 계약 문서와 comments에서 정리한다.
- [ ] `nodes[].providers[]`를 현재 wire의 resource catalog로 해석한다는 compat 기준을 문서화하고, `category=cli|api|local_inference`가 CLI/provider/resource 구분자임을 명시한다.
- [ ] `nodes[].providers[].adapter`가 같은 node 안의 enabled adapter instance로 해석되어야 한다는 config validation을 추가한다.
- [ ] provider adapter reference, duplicate/resource category, legacy config compatibility regression test를 추가한다.
- [ ] proto 변경이 발생하면 `make proto`를 실행하고 generated diff가 원본 proto와 일치하는지 확인한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [API-1] Runtime Concurrency Contract

**문제:** `agent-contract/inner/edge-node-runtime-wire.md`는 `NodeRuntimeConfig.concurrency`를 node-wide 실행 제한으로 정의하고, `apps/edge/internal/configrefresh/classify.go:220`도 live-applyable node-wide concurrency로 주석화한다. 사용자의 방향과 충돌한다.

**해결 방법:** contract, proto comment, Go comments를 `legacy compatibility payload; provider/resource capacity owns admission` 의미로 바꾼다. Wire field를 즉시 제거하지 않는다.

Before:

```proto
// proto/iop/runtime.proto:251
message NodeRuntimeConfig {
  int32  concurrency    = 1;
  reserved 2;
}
```

After:

```proto
// NodeRuntimeConfig carries legacy node runtime metadata. Execution admission
// must not use this as a node-wide global gate; provider/resource capacity owns concurrency.
message NodeRuntimeConfig {
  int32 concurrency = 1; // legacy compatibility, 0/unset means no node-wide limit
  reserved 2;
}
```

**수정 파일 및 체크리스트:**

- [ ] `proto/iop/runtime.proto`
- [ ] `agent-contract/inner/edge-node-runtime-wire.md`
- [ ] `agent-contract/inner/edge-config-runtime-refresh.md`
- [ ] `apps/edge/internal/configrefresh/classify.go`
- [ ] `apps/edge/internal/configrefresh/classify_test.go`

**테스트 작성:** `TestClassifyRuntimeConcurrencyLegacyAppliedOrIgnored` 형태로 기존 `runtime.concurrency` only diff의 기대 의미를 갱신한다. 즉시 제거가 아니면 refresh result는 compat상 `applied`여도 admission 변경을 뜻하지 않는다고 확인한다.

**중간 검증:**

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

### [API-2] Resource Catalog Validation

**문제:** `packages/go/config/config.go:112`의 `NodeProviderConf`는 `Adapter`를 문자열로 받지만, `LoadEdge`의 provider loop(`packages/go/config/config.go:512`)와 `edgevalidate.ValidateEdgeConfig`는 adapter가 같은 node의 enabled adapter instance인지 확인하지 않는다. Edge dispatch의 `providerDispatchable`도 `apps/edge/internal/service/run_dispatch.go:476`에서 non-empty만 본다.

**해결 방법:** `edgevalidate` 또는 config helper에 provider adapter reference resolver를 추가한다. Exact instance key를 우선하고, type-name route는 enabled instance가 정확히 1개일 때만 허용한다. `category=cli` resource는 CLI enabled를 요구한다.

**수정 파일 및 체크리스트:**

- [ ] `apps/edge/internal/edgevalidate/validate.go`
- [ ] `packages/go/config/config.go`
- [ ] `packages/go/config/config_test.go`
- [ ] `apps/edge/internal/edgevalidate/*_test.go` 또는 기존 test package

**테스트 작성:** bad adapter, disabled adapter, ambiguous type route, valid CLI resource, valid OpenAI-compatible provider instance를 table test로 추가한다.

**중간 검증:**

```bash
go test -count=1 ./packages/go/config ./apps/edge/internal/edgevalidate
```

### [API-3] Resource Snapshot Compatibility

**문제:** wire는 `proto/iop/runtime.proto:118`의 `ProviderSnapshot`과 `proto/iop/control.proto:83`의 `provider_snapshots`만 갖는다. 이름은 provider지만 사용자는 CLI/provider 등 resource 목록을 node 아래에 보고 싶어 한다.

**해결 방법:** field rename 없이 `ProviderSnapshot`을 legacy wire name으로 유지하고, 계약 문서에서 `category`가 resource kind를 나타내며 provider-pool 대상은 `models[].providers`에 참조된 catalog resource뿐이라고 명시한다. 실제 status 정리는 03번 subtask에서 수행한다.

**수정 파일 및 체크리스트:**

- [ ] `proto/iop/runtime.proto`
- [ ] `proto/iop/control.proto`
- [ ] `agent-contract/inner/control-plane-edge-wire.md`
- [ ] `agent-contract/inner/edge-config-runtime-refresh.md`

**테스트 작성:** proto field 추가가 없으면 새 generated test는 생략한다. 문서/comment-only면 `go test` regression으로 충분하다.

**중간 검증:**

```bash
go test -count=1 ./proto/gen/...
```

## 수정 파일 요약

| 파일 | 항목 |
|---|---|
| `proto/iop/runtime.proto` | API-1, API-3 |
| `proto/iop/control.proto` | API-3 |
| `agent-contract/inner/edge-node-runtime-wire.md` | API-1 |
| `agent-contract/inner/control-plane-edge-wire.md` | API-3 |
| `agent-contract/inner/edge-config-runtime-refresh.md` | API-1, API-3 |
| `packages/go/config/config.go` | API-2 |
| `packages/go/config/config_test.go` | API-2 |
| `apps/edge/internal/edgevalidate/validate.go` | API-2 |
| `apps/edge/internal/configrefresh/classify.go` | API-1 |
| `apps/edge/internal/configrefresh/classify_test.go` | API-1 |

## 최종 검증

```bash
go test -count=1 ./packages/go/... ./proto/gen/... ./apps/edge/internal/configrefresh ./apps/edge/internal/node
```

Proto 원본을 변경했다면 추가로 실행한다.

```bash
make proto
git diff -- proto/gen/iop
```

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