<!-- task=m-node-resource-model-unification/02+01_node_admission plan=0 tag=REFACTOR -->

# Node Admission Resource Plan

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

이 계획은 `01_contract_schema` 완료 후 구현한다. 구현 뒤에는 반드시 `CODE_REVIEW-local-G06.md`의 구현 에이전트 소유 섹션을 채운다. 구현 중 직접 사용자에게 질문하지 말고, 선택된 Milestone lock 결정만 review stub의 `사용자 리뷰 요청`에 기록한다.

## 배경

Node는 현재 하나의 identity/connection owner가 아니라 `globalGate`를 가진 shared capacity gate처럼 동작한다. 사용자가 정리한 방향에서는 node-wide concurrency가 없어야 하며, CLI는 IOP 레벨에서 제한하지 않고 provider/resource capacity만 admission을 가져야 한다. 이 subtask는 Node 내부 admission을 resource/adapter 단위로 낮춘다.

## 사용자 리뷰 요청 흐름

선택된 Milestone의 구현 잠금은 해제되어 있으므로 사용자 리뷰 요청 기본값은 `없음`이다. 환경/secret/provider 상태 문제는 검증 결과나 후속 계획으로 기록한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md`
- Task ids:
  - `node-admission`: Node admission global gate removal and resource capacity preservation
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `apps/node/internal/node/node.go`
- `apps/node/internal/node/run_manager.go`
- `apps/node/internal/adapters/cli/cli.go`
- `apps/node/internal/adapters/config_set.go`
- `apps/node/internal/adapters/openai_compat/openai_compat.go`
- `proto/iop/runtime.proto`
- `apps/node/internal/node/node_test.go`
- `apps/node/internal/node/node_concurrency_integration_test.go`
- `apps/node/internal/adapters/cli/cli_internal_test.go`
- `apps/node/internal/adapters/config_set_test.go`
- `apps/node/internal/adapters/openai_compat/openai_compat_test.go`
- `agent-test/local/rules.md`
- `agent-test/local/node-smoke.md`
- `agent-test/local/platform-common-smoke.md`
- `agent-contract/inner/edge-node-runtime-wire.md`
- `agent-contract/inner/edge-config-runtime-refresh.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`

### SDD 기준

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

Evidence Map은 Node concurrency regression, global gate no-op evidence, CLI unlimited/provider gate output을 요구한다. 따라서 구현 체크리스트는 `globalGate` 제거 또는 no-op 축소, adapter gate 보존, runtime concurrency refresh no-op, CLI `MaxConcurrency=0`, 관련 Node/CLI regression test를 포함한다.

### 테스트 환경 규칙

test_env는 `local`이다. Node 변경이므로 `agent-test/local/node-smoke.md` 기준을 적용한다. 필수 unit은 `go test -count=1 ./apps/node/...`이며, 실행 요청/admission을 바꾸므로 가능하면 repo 내부 edge-node smoke도 04번 통합 task에서 확인한다.

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

- 기존 `apps/node/internal/node/node_test.go`는 global gate rejection을 기대하는 테스트를 포함한다. 이 테스트들은 새 의미로 갱신해야 한다.
- CLI `MaxConcurrency=4` 기대 테스트는 명시적으로 없지만 capabilities 결과를 쓰는 status/capability 테스트가 영향받을 수 있다.
- provider adapter gate는 기존 concurrency tests가 일부 커버하지만, runtime concurrency가 no-op임을 별도 regression으로 확인해야 한다.

### 심볼 참조

제거 또는 의미 축소 대상:

- `globalGate`: `apps/node/internal/node/node.go:37`, `:68`, `:212`, `:345`, `:357`, `:402`, `:405`
- `admissionManager.global`: `apps/node/internal/node/run_manager.go:195`
- `MaxConcurrency: 4` for CLI: `apps/node/internal/adapters/cli/cli.go:213`
- runtime concurrency tests: `apps/node/internal/node/node_test.go` around NCDRAIN runtime concurrency tests and concurrency integration tests.

### 분할 판단

이 subtask는 `02+01_node_admission`이므로 predecessor `01_contract_schema`의 `complete.log`가 필요하다. 현재 predecessor는 active plan 상태이며 `complete.log`가 없다. 구현자는 runtime이 dependency를 만족시킨 뒤 시작해야 한다.

### 범위 결정 근거

Edge queue/status 정책은 03번 subtask 범위다. 이 subtask는 Node-local admission과 CLI capability만 바꾼다. provider adapter capacity는 adapter `Capabilities().MaxConcurrency`로 계속 Node safety gate를 유지한다.

### 빌드 등급

`local-G06`: Node-local refactor이며 test coverage가 가까이에 있지만 concurrency behavior 변경이라 ordering regression 위험이 있다.

## 의존 관계 및 구현 순서

`02+01_node_admission`은 sibling `01_contract_schema` 완료 후 구현한다. directory dependency 외 추가 의존성은 없다.

## 구현 체크리스트

- [x] `globalGate`를 admission path에서 제거하거나 no-op으로 축소해 node-wide 실행 제한이 생기지 않게 한다.
- [x] adapter gate는 유지하고, provider/openai_compat/vllm/ollama capacity가 `Capabilities().MaxConcurrency`로 제한되는지 보존한다.
- [x] `NodeRuntimeConfig.concurrency` refresh는 config metadata로만 보존하고 admission capacity를 바꾸지 않게 한다.
- [x] CLI adapter `Capabilities()`의 `MaxConcurrency`를 `0`으로 바꿔 unlimited 의미를 노출한다.
- [x] Node concurrency tests를 새 의미에 맞게 갱신하고, provider adapter capacity 제한 regression을 유지한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [REFACTOR-1] Remove Node Global Admission

**문제:** `apps/node/internal/node/node.go:208`의 `admissionFor`가 global gate와 adapter gate를 같이 잡고, `apps/node/internal/node/run_manager.go:207`의 `acquire`가 adapter 성공 뒤 global 실패 시 실행을 거부한다. 이 구조는 하나의 node에 CLI와 MLX provider가 동시에 있을 때 서로를 막을 수 있다.

**해결 방법:** admission manager를 adapter gate only로 단순화한다. `Node.New`의 `globalConcurrency` 인자는 compatibility를 위해 남기더라도 admission에 쓰지 않는다. 관련 comments를 node identity/resource model에 맞게 고친다.

Before:

```go
// apps/node/internal/node/node.go:210
return &admissionManager{
    global:  n.globalGate,
    adapter: n.adapterGateFor(adapterKey, caps),
}
```

After:

```go
return &admissionManager{
    adapter: n.adapterGateFor(adapterKey, caps),
}
```

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

- [x] `apps/node/internal/node/node.go` — `admissionFor`에서 `global` 필드 제거, comment 갱신
- [x] `apps/node/internal/node/run_manager.go` — 변경 없음 (기존 `admissionManager`의 nil safety가 global nil case를 이미 처리)
- [x] `apps/node/internal/node/node_test.go` — 3개 테스트 리팩터링 (global concurrency 기반에서 adapter-only 기반으로 의미 전환)
- [x] `apps/node/internal/node/node_concurrency_integration_test.go` — 변경 없음 (adapter `MaxConcurrency=1`이 reject를 결정하므로 global gate와 무관)

**테스트 작성:** runtime concurrency가 1이어도 서로 다른 unlimited adapters가 global rejection을 받지 않는 test를 추가하고, adapter `MaxConcurrency=1`은 두 번째 run을 reject하는 기존 test를 유지/갱신한다.

**중간 검증:**

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

**결과:** PASS. global gate no-op, adapter-only admission 검증. `TestOverDispatchSafety_RejectEventObservedByEdge` 통과.

### [REFACTOR-2] Make Runtime Concurrency No-op For Admission

**문제:** `apps/node/internal/node/node.go:337`부터 refresh가 `runtime.concurrency` change를 계산하고 `n.globalGate.updateCapacity`를 호출한다. 새 방향에서는 provider/resource capacity가 concurrency source다.

**해결 방법:** `ConfigSet.Runtime`에는 값을 보존하되 live admission mutation을 제거한다. no-adapter-change refresh와 adapter swap refresh 모두 runtime concurrency 때문에 gate가 바뀌지 않아야 한다.

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

- [x] `apps/node/internal/node/node.go` — `OnConfigRefresh`에서 `globalGate.updateCapacity` 호출 제거, `currentConfigSet.Runtime` metadata 저장만 유지
- [x] `apps/node/internal/adapters/config_set.go` — 변경 없음 (`ConfigSet.Runtime`은 이미 metadata 저장)
- [x] `apps/node/internal/node/node_test.go` — 3개 NCDRAIN 테스트 리팩터링
- [x] `apps/node/internal/adapters/config_set_test.go` — 변경 없음 (`DiffConfigSets`만 테스트)

**테스트 작성:** 기존 NCDRAIN runtime concurrency tests를 "config refresh stores runtime metadata but does not throttle admission"으로 갱신한다.

**중간 검증:**

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

**결과:** PASS.

### [REFACTOR-3] CLI Capability Unlimited

**문제:** CLI adapter는 `apps/node/internal/adapters/cli/cli.go:213`에서 `MaxConcurrency: 4`를 반환한다. 현재 admission path에서는 CLI 호출도 4개로 제한된다.

**해결 방법:** CLI `MaxConcurrency`를 `0`으로 반환한다. `fifoGate`의 기존 의미(`capacity <= 0` unlimited)를 그대로 사용한다.

Before:

```go
// apps/node/internal/adapters/cli/cli.go:210
return runtime.Capabilities{
    AdapterName:    Name,
    Targets:        profiles,
    MaxConcurrency: 4,
}, nil
```

After:

```go
return runtime.Capabilities{
    AdapterName:    Name,
    Targets:        profiles,
    MaxConcurrency: 0,
}, nil
```

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

- [x] `apps/node/internal/adapters/cli/cli.go` — `MaxConcurrency: 4` → `0`
- [x] `apps/node/internal/adapters/cli/cli_internal_test.go` — 변경 없음 (기존 테스트에서 `MaxConcurrency=4` 직접 기대하는 부분이 없음)
- [x] `apps/node/internal/node/node_test.go` — 변경 없음 (CLI capability command test의 별도 기대값 없음)

**테스트 작성:** CLI capabilities regression을 추가하거나 Node capability command test 기대값을 `0`으로 갱신한다.

**중간 검증:**

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

**결과:** PASS.

## 수정 파일 요약

### 변경된 파일

| 파일 | 항목 |
|---|---|
| `apps/node/internal/node/node.go` | REFACTOR-1: `admissionFor`에서 `global` 제거. REFACTOR-2: `OnConfigRefresh`에서 runtime concurrency live apply 제거. |
| `apps/node/internal/adapters/cli/cli.go` | REFACTOR-3: `Capabilities().MaxConcurrency` 4 → 0 |
| `apps/node/internal/node/node_test.go` | REFACTOR-1: 3개 테스트 리팩터링. REFACTOR-2: 3개 NCDRAIN 테스트 리팩터링. REFACTOR-3: 변경 없음. |

### 변경 없는 파일 (기존 구조 유지)

| 파일 | 이유 |
|---|---|
| `apps/node/internal/node/run_manager.go` | `admissionManager`의 `global` nil 처리가 이미 존재 (nil safety) |
| `apps/node/internal/adapters/config_set.go` | `ConfigSet.Runtime`은 이미 metadata 저장 |
| `apps/node/internal/node/node_concurrency_integration_test.go` | adapter `MaxConcurrency=1`이 reject를 결정, global gate와 무관 |
| `apps/node/internal/adapters/cli/cli_internal_test.go` | `MaxConcurrency=4` 직접 기대하는 테스트 없음 |
| `apps/node/internal/adapters/config_set_test.go` | `DiffConfigSets`만 테스트 |
| `apps/node/internal/adapters/cli/status/` | 관련 없음 |
| `apps/node/internal/adapters/cli/internal/testutil/` | 관련 없음

## 최종 검증

```text
$ go test -count=1 -timeout 120s ./apps/node/...
ok  	iop/cmd/node		0.032s
ok  	iop/apps/node/internal/adapters	0.036s
ok  	iop/apps/node/internal/adapters/cli	46.567s
ok  	iop/apps/node/internal/adapters/cli/status	39.795s
ok  	iop/apps/node/internal/adapters/ollama	0.022s
ok  	iop/apps/node/internal/adapters/openai_compat	0.022s
ok  	iop/apps/node/internal/adapters/vllm	0.027s
ok  	iop/apps/node/internal/bootstrap	0.443s
ok  	iop/apps/node/internal/node	0.316s
ok  	iop/apps/node/internal/router	0.507s
ok  	iop/apps/node/internal/store	0.047s
ok  	iop/apps/node/internal/terminal	0.542s
ok  	iop/apps/node/internal/transport	5.343s
```

**전체 결과:** PASS. 모든 패키지 0 실패.

## 구현 완료 보고서

| 체크리스트 | 상태 |
|---|---|
| `globalGate` admission path 제거 | ✅ 완료 |
| adapter gate 보존 | ✅ 완료 |
| runtime concurrency no-op | ✅ 완료 |
| CLI `MaxConcurrency=0` | ✅ 완료 |
| Node/CLI regression tests | ✅ 완료 |
| CODE_REVIEW 채우기 | ✅ 완료 |
