<!-- task=m-runtime-reconnect-config-refresh/05+04_node_adapter_diff plan=0 tag=NCADAPT -->

# Plan - NCADAPT

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

`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션 작성은 필수다. 구현 후 검증을 실행하고, 실제 구현 내용과 실제 stdout/stderr를 리뷰 stub에 채운 뒤 active 파일을 그대로 두고 리뷰 준비를 보고한다. 최종 archive, `complete.log`, roadmap 완료 처리는 code-review/runtime 책임이다. 선택된 Milestone의 `구현 잠금 > 결정 필요` 항목 때문에 구현이 막힐 때만 리뷰 stub의 `사용자 리뷰 요청` 섹션에 정확한 근거를 적고 멈춘다. 구현 중 사용자에게 직접 질문하거나 선택지를 제시하거나 `request_user_input`을 호출하거나, `USER_REVIEW.md`/`complete.log`를 직접 만들지 않는다. 환경/secret/service 차단, 일반 범위 조정, 후속 에이전트가 닫을 수 있는 evidence 공백은 사용자 리뷰 요청이 아니다.

## 배경

Edge가 refresh payload를 보낼 계약이 생겨도 Node가 adapter registry를 갱신하지 못하면 provider capacity/endpoint 변경은 running Node에 반영되지 않는다. 현재 Node bootstrap은 initial payload로 registry/router/gates를 한 번 만들고 이후 교체 경로가 없다. 이 계획은 in-flight drain 정책을 완성하지 않고, 변경분을 계산해 새 registry snapshot을 라우팅에 반영하는 `adapter-diff`만 완료한다.

## 사용자 리뷰 요청 흐름

구현 중 사용자 리뷰 요청은 선택된 Milestone lock decision이 실제 구현을 막을 때만 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/update-plane-self-update-foundation/milestones/runtime-reconnect-config-refresh.md`
- Task ids:
  - `adapter-diff`: Node는 adapter instance 추가/수정/삭제, provider endpoint/header/capacity 변경을 diff로 적용한다. 검증: openai_compat provider capacity 변경과 endpoint 변경이 adapter registry에 반영된다.
- 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-ops/rules/project/domain/node/rules.md`
- `agent-ops/rules/project/domain/edge/rules.md`
- `agent-ops/rules/project/domain/platform-common/rules.md`
- `agent-ops/rules/project/domain/testing/rules.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-roadmap/current.md`
- `agent-roadmap/phase/update-plane-self-update-foundation/PHASE.md`
- `agent-roadmap/phase/update-plane-self-update-foundation/milestones/runtime-reconnect-config-refresh.md`
- `agent-roadmap/sdd/update-plane-self-update-foundation/runtime-reconnect-config-refresh/SDD.md`
- `proto/iop/runtime.proto`
- `apps/node/internal/bootstrap/module.go`
- `apps/node/internal/transport/session.go`
- `apps/node/internal/node/node.go`
- `apps/node/internal/node/run_manager.go`
- `apps/node/internal/router/router.go`
- `apps/node/internal/adapters/registry.go`
- `apps/node/internal/adapters/factory.go`
- `apps/node/internal/adapters/openai_compat/openai_compat.go`
- `apps/node/internal/runtime/types.go`
- `apps/node/internal/adapters/factory_internal_test.go`
- `apps/node/internal/adapters/adapters_blackbox_test.go`
- `apps/node/internal/adapters/openai_compat/openai_compat_test.go`
- `apps/node/internal/router/router_test.go`
- `apps/node/internal/node/node_test.go`
- `apps/node/internal/node/node_concurrency_integration_test.go`
- `apps/node/internal/bootstrap/module_test.go`

### SDD 기준

- SDD: `agent-roadmap/sdd/update-plane-self-update-foundation/runtime-reconnect-config-refresh/SDD.md`
- 상태: 승인됨, SDD 잠금 해제.
- 대상 Acceptance Scenario: S13 / Milestone Task `adapter-diff`.
- Evidence Map: S13은 `agent-task/m-runtime-reconnect-config-refresh/04+02_node_config_apply`에 연결되어 있으며 "Node adapter registry diff test for openai_compat"와 `adapter-diff`: capacity/endpoint changes reflected를 요구한다.
- 구현 checklist는 Node refresh payload 수신 이후 fresh registry build, diff summary, mutable router swap, openai_compat capacity/endpoint assertion을 중심으로 구성한다.

### 테스트 환경 규칙

- test_env: `local`.
- `agent-test/local/node-smoke.md`는 Node 변경 시 `go test ./apps/node/...`를 요구한다.
- `agent-test/local/platform-common-smoke.md`는 proto/generated 직접 수정 금지를 요구한다. 이 plan은 proto 계약을 새로 바꾸지 않고 NCPUSH generated types를 소비한다.
- full-cycle 실제 구동은 field task 범위이므로 이 plan의 PASS 조건에는 포함하지 않는다.

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

- Registry diff: 현재 `BuildFromPayload` tests는 fresh registry 생성만 검증하고, 기존 registry와 next payload의 diff/swap은 검증하지 않는다.
- Router live swap: 현재 `router.New`는 registry pointer를 고정해 live update test가 없다.
- openai_compat capacity/endpoint: factory tests는 payload parse를 보지만 refresh 후 registry에 반영되는지 확인하지 않는다.
- Drain: in-flight run 중 old adapter snapshot 유지와 stop 지연은 이 plan에서 제외하고 `06+05_node_drain_runtime_config`가 검증한다.

### 심볼 참조

- rename/remove 없음.
- `transport.Handler.OnConfigRefresh`는 predecessor NCPUSH에서 추가될 예정이며 이 plan은 그 method body를 adapter apply로 확장한다.
- `router.New` 반환 타입 또는 interface 확장이 필요하면 call sites는 `apps/node/internal/bootstrap/module.go`, `apps/node/internal/node/node_test.go`, `apps/node/internal/router/router_test.go`를 갱신한다.

### 분할 판단

- split decision policy를 먼저 평가했다. Adapter registry diff는 protocol push 계약 뒤에 구현 가능하고, drain/runtime-config는 concurrency와 persisted store boundary가 달라 후속 plan으로 분리한다.
- 공유 task group: `m-runtime-reconnect-config-refresh`.
- sibling subtask:
  - `04+02_config_push_contract`: predecessor `02` 충족 후 작성된 active plan. 이 subtask의 direct predecessor다.
  - `05+04_node_adapter_diff`: predecessor `04`는 현재 missing이다. 구현 시작 전 `agent-task/archive/**/m-runtime-reconnect-config-refresh/04+02_config_push_contract/complete.log` 또는 active `complete.log`가 필요하다.
  - `06+05_node_drain_runtime_config`: predecessor `05` missing.

### 범위 결정 근거

- 포함: Node-side payload apply model, registry diff calculation, mutable router registry swap, openai_compat capacity/endpoint/header change application, tests.
- 제외: Edge push contract/proto generation(NCPUSH), in-flight run drain and deferred stop(NCDRAIN), runtime concurrency/workspace root policy(NCDRAIN), field validation.
- Adapter 변경은 fresh adapter instance build + atomic router swap으로 처리한다. Per-adapter in-flight stop 지연은 이 plan에서 구현하지 않으며, changed/removed adapter stop은 active run이 없을 때만 즉시 수행하거나 NCDRAIN으로 넘길 수 있게 결과에 표시한다.

### 빌드 등급

- lane/GNN: `cloud-G07`.
- 이유: Node runtime shared state, router interface, adapter lifecycle, tests가 함께 바뀌며 후속 drain plan과 의존성이 있다.

## 구현 체크리스트

- [ ] `04+02_config_push_contract`의 `complete.log`가 active/archive 중 하나에 있는지 확인하고, 없으면 구현하지 말고 review stub `검증 결과`에 dependency blocker로 기록한다.
- [ ] Node adapter config set/diff 모델을 추가해 adapter instance 추가/수정/삭제와 openai_compat endpoint/header/capacity 변경을 식별한다. 검증: registry diff unit test가 added/updated/removed/unchanged를 확인한다.
- [ ] Router를 live registry swap 가능하게 만들고 Node `OnConfigRefresh`가 payload build, diff, registry start/swap, ack response를 수행하게 한다. 검증: Node handler test가 refresh 후 후속 resolve/capabilities에서 새 openai_compat capacity를 확인한다.
- [ ] openai_compat endpoint/header 변경이 새 adapter instance에 반영되는지 httptest provider로 확인한다. 검증: 기존 endpoint와 새 endpoint 중 refresh 후 새 endpoint만 호출된다.
- [ ] 전체 Node 회귀로 `go test -count=1 ./apps/node/...`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

- 이 subtask는 `05+04_node_adapter_diff`이므로 predecessor `04`만 요구한다.
- predecessor 상태: missing. 구현 시작 전 `agent-task/m-runtime-reconnect-config-refresh/04+02_config_push_contract/complete.log` 또는 `agent-task/archive/**/m-runtime-reconnect-config-refresh/04+02_config_push_contract/complete.log`가 있어야 한다.
- predecessor가 없으면 소스 코드를 수정하지 말고 active `CODE_REVIEW-cloud-G07.md`에 dependency blocker를 기록해 code-review로 넘긴다.

### [NCADAPT-1] Adapter Config Diff Model

#### 문제

`apps/node/internal/adapters/factory.go:18`은 payload에서 fresh `Registry`만 만든다. `apps/node/internal/adapters/registry.go:21`의 `Registry`는 entries/order만 보유하고 payload fingerprint나 diff API가 없다.

Before `apps/node/internal/adapters/factory.go:18`:

```go
// BuildFromPayload creates a Registry from a NodeConfigPayload received from edge.
func BuildFromPayload(payload *iop.NodeConfigPayload, logger *zap.Logger) (*Registry, error) {
	reg := NewRegistry()
```

#### 해결 방법

`adapters` package에 config set builder를 추가해 payload adapter별 stable key, type, normalized config fingerprint, constructed adapter를 함께 반환한다. Diff는 old set과 next set을 비교해 `Added`, `Updated`, `Removed`, `Unchanged`를 만든다. Existing `BuildFromPayload`는 새 builder를 호출하도록 유지해 backward compatibility를 지킨다.

After:

```go
type ConfigSet struct {
	Registry *Registry
	Items map[string]ConfigItem
}

type ConfigItem struct {
	Key string
	Type string
	Fingerprint string
}

func BuildConfigSet(payload *iop.NodeConfigPayload, logger *zap.Logger) (*ConfigSet, error) {
	// normalizes typed oneof and legacy settings before fingerprinting.
}

func DiffConfigSets(current, next *ConfigSet) ConfigDiff {
	// returns added/updated/removed/unchanged keys.
}
```

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

- [ ] `apps/node/internal/adapters/factory.go`: `BuildFromPayload`를 `BuildConfigSet` 기반으로 유지.
- [ ] `apps/node/internal/adapters/registry.go`: needed read-only metadata accessors만 추가.
- [ ] `apps/node/internal/adapters/config_set.go` 또는 기존 파일 내 helper: diff model 구현.
- [ ] `apps/node/internal/adapters/factory_internal_test.go`/`adapters_blackbox_test.go`: diff tests 추가.

#### 테스트 작성

- `TestDiffConfigSetsOpenAICompatCapacityEndpoint`: capacity/endpoint/header 변경이 updated로 잡히는지 확인.
- `TestDiffConfigSetsAddedRemovedUnchanged`: add/remove/no-op 분류 확인.

#### 중간 검증

```bash
go test -count=1 ./apps/node/internal/adapters
```

기대 결과: adapter factory/diff tests PASS.

### [NCADAPT-2] Mutable Router And Node Apply

#### 문제

`apps/node/internal/router/router.go:13`의 `defaultRouter`는 registry pointer를 고정한다. `apps/node/internal/bootstrap/module.go:103`은 initial registry로 router를 만들고, `apps/node/internal/node/node.go:93`은 그 router를 통해 adapter를 resolve한다. Refresh 후 새 registry를 route path에 반영할 방법이 없다.

Before `apps/node/internal/router/router.go:13`:

```go
type defaultRouter struct {
	registry *adapters.Registry
	logger   *zap.Logger
}
```

#### 해결 방법

Router에 synchronized registry holder를 추가한다. `Resolve`, `ResolveAdapter`, `LookupAdapter`, `GetAdapter`는 현재 registry snapshot을 읽고, `SetRegistry`는 fresh registry가 start된 뒤 atomic하게 교체한다. Node는 current `ConfigSet`을 보유하고 `OnConfigRefresh`에서 next set build/diff/start/swap/response를 수행한다.

After:

```go
type MutableRouter interface {
	runtime.Router
	SetRegistry(*adapters.Registry)
}

type defaultRouter struct {
	mu sync.RWMutex
	registry *adapters.Registry
	logger *zap.Logger
}

func (r *defaultRouter) SetRegistry(reg *adapters.Registry) {
	r.mu.Lock()
	defer r.mu.Unlock()
	r.registry = reg
}
```

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

- [ ] `apps/node/internal/router/router.go`: mutable registry holder 추가.
- [ ] `apps/node/internal/bootstrap/module.go`: concrete mutable router와 initial config set을 Node에 전달.
- [ ] `apps/node/internal/node/node.go`: `OnConfigRefresh`가 config set diff를 적용하고 response status를 반환.
- [ ] `apps/node/internal/router/router_test.go`: swap 후 resolve가 새 registry를 보는지 확인.
- [ ] `apps/node/internal/node/node_test.go`: refresh 후 capabilities/resolve가 새 capacity를 보는지 확인.

#### 테스트 작성

- `TestRouterSetRegistryAffectsSubsequentResolve`: old registry에는 없는 adapter가 swap 후 resolve된다.
- `TestNodeConfigRefreshAppliesOpenAICompatCapacity`: refresh 후 command capabilities가 새 capacity를 반환한다.

#### 중간 검증

```bash
go test -count=1 ./apps/node/internal/router ./apps/node/internal/node
```

기대 결과: router swap과 Node apply tests PASS.

### [NCADAPT-3] OpenAI Compat Endpoint/Header Verification

#### 문제

`apps/node/internal/adapters/factory.go:93`는 openai_compat proto config를 `config.OpenAICompatConf`로 변환하지만, refresh 후 새 endpoint/header가 실제 adapter instance에 들어갔는지 확인하는 테스트가 없다.

Before `apps/node/internal/adapters/factory.go:93`:

```go
func openAICompatConfFromProto(m *iop.OpenAICompatAdapterConfig) config.OpenAICompatConf {
	return config.OpenAICompatConf{
		Enabled:          true,
		Provider:         m.GetProvider(),
		Endpoint:         m.GetEndpoint(),
		Headers:          m.GetHeaders(),
```

#### 해결 방법

`httptest.Server` 두 개를 만들고 refresh 전/후 openai_compat adapter command/probe/execute 중 가장 좁은 기존 public path로 endpoint 호출을 관측한다. Header 변경은 server-side request assertion으로 검증한다. 새 adapter가 router에 반영된 뒤 후속 request만 새 endpoint로 가야 한다.

After:

```go
oldCalls := atomic.Int32{}
newCalls := atomic.Int32{}
// initial payload uses old server, refresh payload uses new server.
// after refresh, a follow-up capability/probe/execute path hits only new server.
```

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

- [ ] `apps/node/internal/adapters/openai_compat/openai_compat_test.go` 또는 Node integration test: endpoint/header refresh assertion 추가.
- [ ] `apps/node/internal/node/node_test.go`: Node refresh response가 updated adapter keys를 result metadata에 담는다면 assertion 추가.

#### 테스트 작성

- `TestNodeConfigRefreshUsesUpdatedOpenAICompatEndpoint`: refresh 후 후속 provider interaction이 새 endpoint로 간다.
- `TestNodeConfigRefreshUsesUpdatedOpenAICompatHeaders`: configured header가 새 endpoint request에 포함된다.

#### 중간 검증

```bash
go test -count=1 ./apps/node/internal/adapters/openai_compat ./apps/node/internal/node
```

기대 결과: endpoint/header refresh assertions PASS.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/node/internal/adapters/factory.go` | NCADAPT-1 |
| `apps/node/internal/adapters/registry.go` | NCADAPT-1 |
| `apps/node/internal/adapters/config_set.go` | NCADAPT-1 |
| `apps/node/internal/adapters/factory_internal_test.go` | NCADAPT-1 |
| `apps/node/internal/adapters/adapters_blackbox_test.go` | NCADAPT-1 |
| `apps/node/internal/router/router.go` | NCADAPT-2 |
| `apps/node/internal/bootstrap/module.go` | NCADAPT-2 |
| `apps/node/internal/node/node.go` | NCADAPT-2 |
| `apps/node/internal/router/router_test.go` | NCADAPT-2 |
| `apps/node/internal/node/node_test.go` | NCADAPT-2, NCADAPT-3 |
| `apps/node/internal/adapters/openai_compat/openai_compat_test.go` | NCADAPT-3 |

## 최종 검증

```bash
go test -count=1 ./apps/node/internal/adapters
go test -count=1 ./apps/node/internal/router ./apps/node/internal/node
go test -count=1 ./apps/node/internal/adapters/openai_compat ./apps/node/internal/node
go test -count=1 ./apps/node/...
```

기대 결과: adapter diff, router swap, Node refresh apply, openai_compat endpoint/header assertions PASS. Go test cache output은 허용하지 않는다.
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
