<!-- task=m-edge-local-dev-config-runtime/02+01_node_register_bootstrap plan=0 tag=BOOTSTRAP -->

# Plan - BOOTSTRAP

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

`CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션 작성은 필수다. 구현 후 검증을 실행하고 실제 출력과 구현 메모를 채운 뒤 active 파일을 그대로 두고 리뷰 준비를 보고한다. finalization은 code-review-skill 전용이다.

## 배경

현재 Edge config의 `nodes[]`는 런타임 등록 source of truth지만 사용자가 직접 YAML을 편집해야 한다. Milestone은 `iop-edge node register <node-id>`가 node record를 생성/갱신하고, 바로 실행 가능한 one-line bootstrap command를 출력하는 UX를 요구한다. 이 작업은 config/env 기반이 완성된 뒤 진행해야 한다.

## 분석 결과

### 읽은 파일

- `agent-roadmap/current.md`
- `agent-roadmap/phase/control-plane-portal-ops/PHASE.md`
- `agent-roadmap/phase/control-plane-portal-ops/milestones/edge-local-dev-config-runtime.md`
- `apps/edge/cmd/edge/main.go`
- `apps/edge/cmd/edge/main_test.go`
- `apps/edge/internal/node/store.go`
- `apps/edge/internal/node/store_test.go`
- `apps/edge/internal/bootstrap/runtime.go`
- `apps/edge/internal/bootstrap/runtime_test.go`
- `packages/config/config.go`
- `packages/config/config_test.go`
- `configs/edge.yaml`
- `bin/build/field-binaries.sh`
- `docs/field-bootstrap-user-test.md`
- `docs/field-bootstrap-work-guide.md`
- `docs/deploy-dev.md`

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

- `config check` loads YAML but does not exercise `edgenode.LoadFromConfig` from the CLI path.
- `edgenode.LoadFromConfig` covers duplicate token/alias/id and empty token, but not adapter/runtime semantic validation.
- No `node` or `node register` command exists.
- Bootstrap scripts exist in `bin/build/field-binaries.sh`, but Edge CLI does not render the one-line command from config.

### 심볼 참조

- none. No rename/remove is planned.

### 분할 판단

Split decision policy was evaluated before choosing plan files. Shared task group: `agent-task/m-edge-local-dev-config-runtime/`.
- `01_config_runtime_env` must finish first because this task needs resolved config path, advertise host, artifact URL, and Node transport address.
- `02+01_node_register_bootstrap` adds config validation and mutation.
- `03+01,02_help_smoke_bundle_docs` waits for this task because help and docs must describe the real register/bootstrap commands.

### 범위 결정 근거

Do not change the node transport handshake, bootstrap shell script internals, artifact build layout, Control Plane enrollment, or Node user-facing config commands here. The command may write an implementation detail config consumed by existing bootstrap scripts, but user-facing output must stay one-line and token-positional.

### 빌드 등급

build=`cloud-G07`, review=`cloud-G07`. This is CLI workflow, YAML mutation, token generation, bootstrap command rendering, and config validation with user-facing side effects.

## 구현 체크리스트

- [ ] Verify `agent-task/m-edge-local-dev-config-runtime/01_config_runtime_env/complete.log` exists before implementation starts.
- [ ] `iop-edge config check` validates `nodes[]` id/alias/token collisions and adapter/runtime errors through the same semantics used by runtime seed.
- [ ] `iop-edge node register <node-id>` creates or updates one `nodes[]` record from flags/defaults without requiring manual YAML edits.
- [ ] Register output ends with a single user-facing bootstrap command in `curl -fsSL <complete-url> | bash -s <token>` form and does not require named `IOP_*=` parameters.
- [ ] Token generation/upsert behavior is deterministic in tests and does not leak full token in extra diagnostic output.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

Directory dependency is `02+01_node_register_bootstrap`: wait for `agent-task/m-edge-local-dev-config-runtime/01_config_runtime_env/complete.log`. Do not add hidden dependencies beyond that.

### [BOOTSTRAP-1] Config Check Uses Runtime Node Validation

#### 문제

[apps/edge/cmd/edge/main.go](/config/workspace/iop/apps/edge/cmd/edge/main.go:148) only calls `config.LoadEdge`, while [apps/edge/internal/node/store.go](/config/workspace/iop/apps/edge/internal/node/store.go:67) is where duplicate token/alias/id and empty token are rejected for runtime.

Before:

```go
// apps/edge/cmd/edge/main.go:153
if _, err := config.LoadEdge(cfgFile); err != nil {
    return fmt.Errorf("load config: %w", err)
}
```

#### 해결 방법

After loading the resolved config, call an edge CLI validator that delegates node seed validation to `edgenode.LoadFromConfig(cfg.Nodes)` and adds adapter/runtime checks. Keep validation in `apps/edge/cmd/edge` or `apps/edge/internal/node` rather than `packages/config` to avoid importing app internals into common packages.

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

- [ ] `apps/edge/cmd/edge/main.go`: add `validateEdgeConfig` call in `config check`.
- [ ] `apps/edge/cmd/edge/main_test.go`: add CLI tests for duplicate token/id/alias and invalid adapter/runtime flags.
- [ ] `apps/edge/internal/node/store.go`: add semantic validation only if it belongs to runtime seed; otherwise keep CLI-only validation in command package.
- [ ] `apps/edge/internal/node/store_test.go`: extend only if store behavior changes.

#### 테스트 작성

Write `TestConfigCheckRejectsDuplicateNodeToken` and at least one adapter/runtime invalid config test.

#### 중간 검증

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

Expected: pass; cache output is not acceptable.

### [BOOTSTRAP-2] Node Register Upserts Edge YAML

#### 문제

[docs/field-bootstrap-user-test.md](/config/workspace/iop/docs/field-bootstrap-user-test.md:75) says Node definitions are made with `iop-edge node register`, but [apps/edge/cmd/edge/main.go](/config/workspace/iop/apps/edge/cmd/edge/main.go:35) has no `node` command.

Before:

```go
// apps/edge/cmd/edge/main.go:35
root.AddCommand(serveCmd(), consoleCmd(), versionCmd(), configCmd(), setupCmd())
```

#### 해결 방법

Add `nodeCmd()` with `register` subcommand. Load resolved edge config and YAML, upsert by `id`, default alias to `id`, generate token if not provided, and support initial flags:

```text
iop-edge node register <node-id>
  --alias <alias>
  --token <token>
  --adapter ollama|cli
  --ollama-base-url <url>
  --ollama-context-size <n>
  --runtime-concurrency <n>
  --workspace-root <path>
```

Use `gopkg.in/yaml.v3` already present in `apps/edge/cmd/edge/main.go` for writing. Keep output concise: write/update line plus final bootstrap command.

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

- [ ] `apps/edge/cmd/edge/main.go`: add node/register command, token generator injection for tests, YAML upsert helpers.
- [ ] `apps/edge/cmd/edge/main_test.go`: test create, update, generated token, explicit token, adapter flags, and no manual `nodes[]` edit path.
- [ ] `packages/config/config.go`: add fields only if BOOTSTRAP-2 needs config surface not already added by CONFIG-2.
- [ ] `configs/edge.yaml`: do not add field tokens or host-specific generated values.

#### 테스트 작성

Write temp-file CLI tests that run `config init --output`, then `node register`, then `config check`. Assert exactly one node record after update and that generated token is present in YAML and bootstrap output.

#### 중간 검증

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

Expected: pass; cache output is not acceptable.

### [BOOTSTRAP-3] Bootstrap Command Rendering

#### 문제

[bin/build/field-binaries.sh](/config/workspace/iop/bin/build/field-binaries.sh:83) generates target-specific bootstrap scripts, but [docs/field-bootstrap-user-test.md](/config/workspace/iop/docs/field-bootstrap-user-test.md:96) expects Edge CLI to output the complete one-line command.

Before:

```bash
# docs/field-bootstrap-user-test.md:96
curl -fsSL http://toki-labs.com:18080/bootstrap/node-darwin-arm64.sh | bash -s <generated-token>
```

#### 해결 방법

Render command from CONFIG-2 effective artifact base URL and default platform target. Provide flags on `node register`:

```text
--target darwin-arm64
--artifact-base-url <url>
```

Default target can be current host `GOOS-GOARCH` for local dev, but tests should pass explicit `--target darwin-arm64`. The final visible command must be exactly one line with completed URL and token positional arg. Do not output `IOP_ARTIFACT_BASE_URL=`, `IOP_EDGE_ADDR=`, or `IOP_NODE_TOKEN=` in the default path.

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

- [ ] `apps/edge/cmd/edge/main.go`: add bootstrap command renderer helper and register output.
- [ ] `apps/edge/cmd/edge/main_test.go`: assert one-line command, complete URL, token positional arg, and absence of named env parameters.
- [ ] `docs/field-bootstrap-work-guide.md`: no change in this subtask unless a field constant changes; defer broad docs to subtask 03.

#### 테스트 작성

Write `TestNodeRegisterPrintsOneLineBootstrapCommand` and `TestNodeRegisterUsesConfiguredArtifactBaseURL`.

#### 중간 검증

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

Expected: pass; cache output is not acceptable.

## 수정 파일 요약

| 파일 | 항목 |
|---|---|
| `apps/edge/cmd/edge/main.go` | BOOTSTRAP-1, BOOTSTRAP-2, BOOTSTRAP-3 |
| `apps/edge/cmd/edge/main_test.go` | BOOTSTRAP-1, BOOTSTRAP-2, BOOTSTRAP-3 |
| `apps/edge/internal/node/store.go` | BOOTSTRAP-1 |
| `apps/edge/internal/node/store_test.go` | BOOTSTRAP-1 |
| `packages/config/config.go` | BOOTSTRAP-2 |
| `packages/config/config_test.go` | BOOTSTRAP-2 |

## 최종 검증

```bash
test -f agent-task/m-edge-local-dev-config-runtime/01_config_runtime_env/complete.log
```

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

```bash
tmp="$(mktemp -d)" && go build -o "$tmp/iop-edge" ./apps/edge/cmd/edge && (cd "$tmp" && ./iop-edge config init --force && ./iop-edge node register node-silicon-ollama --adapter ollama --ollama-base-url http://127.0.0.1:11434 --target darwin-arm64 --artifact-base-url http://toki-labs.com:18080 && ./iop-edge config check)
```

Expected: predecessor exists; tests pass; temp command creates/updates `edge.yaml`, prints one final `curl -fsSL http://toki-labs.com:18080/bootstrap/node-darwin-arm64.sh | bash -s <token>` command, and config check passes.

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