<!-- task=m-edge-local-dev-config-runtime/01_config_runtime_env plan=0 tag=CONFIG -->

# Plan - CONFIG

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

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

## 배경

현재 `iop-edge`는 기본 config 경로가 repo 상대 `configs/edge.yaml`에 고정되어 있어 배포 bundle 단독 실행 경로와 맞지 않는다. 사용자용 가이드는 `./iop-edge env`, 자동 advertise host, bundle-local logs를 전제로 하지만 CLI/config/runtime에는 아직 해당 계약이 없다. 이 subtask는 후속 Node register/bootstrap이 사용할 config path, address, log 위치의 기반을 먼저 만든다.

## 분석 결과

### 읽은 파일

- `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/bootstrap/runtime.go`
- `apps/edge/internal/bootstrap/runtime_test.go`
- `packages/config/config.go`
- `packages/config/config_test.go`
- `packages/hostsetup/templates.go`
- `packages/hostsetup/setup.go`
- `packages/hostsetup/setup_test.go`
- `packages/observability/observability.go`
- `configs/edge.yaml`
- `docs/field-bootstrap-user-test.md`
- `docs/field-bootstrap-work-guide.md`
- `docs/deploy-dev.md`

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

- Config discovery: existing CLI tests cover explicit `--config`, but no test covers default bundle `edge.yaml` discovery.
- `env` report: no command or test exists.
- advertise host: no config field, resolver, or empty-vs-explicit test exists.
- log defaults: `observability.NewLogger` only writes stderr; no file logger or bundle-local default test exists.

### 심볼 참조

- 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`: independent foundation for config discovery, env report, advertise host, and log defaults.
- `02+01_node_register_bootstrap`: depends on `01_config_runtime_env` because bootstrap command rendering needs resolved advertise/artifact addresses and config path behavior.
- `03+01,02_help_smoke_bundle_docs`: depends on both earlier subtasks because help, smoke, and docs must describe implemented command behavior.

### 범위 결정 근거

Do not implement Node register, bootstrap script generation, smoke execution, or docs rewrite here. Do not change Edge-Node transport protocol, OpenAI request behavior, Control Plane sync, or Node CLI UX. Keep `setup` default `/etc/iop/edge.yaml` behavior intact.

### 빌드 등급

build=`cloud-G07`, review=`cloud-G07`. This is terminal/CLI workflow work with config path contracts, stdout output, logging side effects, and full-cycle verification requirements.

## 구현 체크리스트

- [ ] `iop-edge` default config resolution uses explicit `--config` first, then bundle-local `edge.yaml`, without breaking setup defaults.
- [ ] `iop-edge env` prints resolved config path, binary dir, log path, advertise host, Node transport address, artifact/bootstrap URL, OpenAI-compatible URL, configured node summary, and connected node summary placeholder/offline state.
- [ ] Empty advertise host is resolved from host interfaces and explicit advertise host wins over auto-detection.
- [ ] Empty local/dev log path resolves to `<binary-dir>/logs/edge.log`, creates the log directory on `serve`, and explicit log path wins.
- [ ] Verification covers `config-discovery`, `env-report`, `advertise-host`, and `log-defaults` roadmap checks with deterministic CLI/unit tests.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

This subtask has no predecessor. `02+01_node_register_bootstrap` and `03+01,02_help_smoke_bundle_docs` must wait for this subtask's `complete.log`.

### [CONFIG-1] Bundle Config Discovery

#### 문제

[apps/edge/cmd/edge/main.go](/config/workspace/iop/apps/edge/cmd/edge/main.go:34) fixes the persistent `--config` default to `configs/edge.yaml`, and [docs/field-bootstrap-user-test.md](/config/workspace/iop/docs/field-bootstrap-user-test.md:105) expects `./iop-edge config check` to work in a bundle directory.

Before:

```go
// apps/edge/cmd/edge/main.go:34
root.PersistentFlags().StringVarP(&cfgFile, "config", "c", "configs/edge.yaml", "config file path")
```

#### 해결 방법

Add an edge CLI config resolver in `apps/edge/cmd/edge/main.go`:

```go
// apps/edge/cmd/edge/main.go:34
root.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file path")
```

Use `cmd.Root().PersistentFlags().Lookup("config").Changed` or equivalent to distinguish explicit config. Candidate order: explicit `--config`, current working directory `edge.yaml`, executable directory `edge.yaml`, then repo-dev fallback `configs/edge.yaml` only if it exists. Return the resolved path and use it in `serve`, `console`, `config print`, `config check`, and `env`; keep `setup` default logic on `/etc/iop/edge.yaml`.

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

- [ ] `apps/edge/cmd/edge/main.go`: add resolver helper and replace direct `config.LoadEdge(cfgFile)` calls.
- [ ] `apps/edge/cmd/edge/main_test.go`: add temp bundle test where only `iop-edge` test process cwd plus `edge.yaml` exists and `config check` succeeds without `--config`.

#### 테스트 작성

Write `TestConfigCheckUsesBundleEdgeYAMLByDefault` and `TestExplicitConfigWinsOverBundleDefault` in `apps/edge/cmd/edge/main_test.go`.

#### 중간 검증

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

Expected: pass; cache output is not acceptable because config discovery depends on temp directories.

### [CONFIG-2] Env Report And Advertise Host

#### 문제

[docs/field-bootstrap-user-test.md](/config/workspace/iop/docs/field-bootstrap-user-test.md:47) documents `./iop-edge env`, but [apps/edge/cmd/edge/main.go](/config/workspace/iop/apps/edge/cmd/edge/main.go:35) has no `env` command. [packages/config/config.go](/config/workspace/iop/packages/config/config.go:46) has only `server.listen`; there is no advertise host or artifact/bootstrap URL field.

Before:

```go
// packages/config/config.go:46
type EdgeServerConf struct {
    Listen string `mapstructure:"listen" yaml:"listen"`
}
```

#### 해결 방법

Extend config minimally:

```go
type EdgeServerConf struct {
    Listen        string `mapstructure:"listen" yaml:"listen"`
    AdvertiseHost string `mapstructure:"advertise_host" yaml:"advertise_host"`
}

type EdgeBootstrapConf struct {
    ArtifactBaseURL string `mapstructure:"artifact_base_url" yaml:"artifact_base_url"`
}
```

Add `Bootstrap EdgeBootstrapConf` to `EdgeConfig`. `env` should print stable `key: value` lines: config path, binary dir, log path, advertise host, node transport address, artifact/bootstrap URL, OpenAI-compatible URL, configured nodes, connected nodes. If no runtime is running, print connected nodes as `offline` or `0 (env command does not start edge)`. Auto advertise host should prefer explicit `server.advertise_host`, else non-loopback IPv4, else hostname, else `127.0.0.1`.

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

- [ ] `packages/config/config.go`: add config fields and defaults.
- [ ] `packages/config/config_test.go`: add empty/default and explicit advertise/artifact tests.
- [ ] `apps/edge/cmd/edge/main.go`: add `env` command and deterministic env renderer helpers.
- [ ] `apps/edge/cmd/edge/main_test.go`: add `env` output tests for explicit advertise host and artifact URL.
- [ ] `configs/edge.yaml`: include new empty/field-like keys without putting field tokens in defaults.

#### 테스트 작성

Write unit tests for config unmarshal/defaults and CLI env rendering. Avoid asserting the real machine IP; test explicit advertise host and structure of auto output.

#### 중간 검증

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

Expected: pass; cache output is not acceptable.

### [CONFIG-3] Bundle Log Defaults

#### 문제

[docs/field-bootstrap-user-test.md](/config/workspace/iop/docs/field-bootstrap-user-test.md:67) expects logs under `/opt/iop-dev/logs/`, but [packages/config/config.go](/config/workspace/iop/packages/config/config.go:121) has only `LoggingConf.Level/Pretty`, and [apps/edge/internal/bootstrap/runtime.go](/config/workspace/iop/apps/edge/internal/bootstrap/runtime.go:39) creates a stderr logger only.

Before:

```go
// apps/edge/internal/bootstrap/runtime.go:39
logger, err := observability.NewLogger(cfg.Logging.Level, cfg.Logging.Pretty)
```

#### 해결 방법

Add `LoggingConf.Path string` or `LoggingConf.File string` in `packages/config`. Resolve empty edge log path in the CLI before `bootstrap.Module(cfg)` to `<binary-dir>/logs/edge.log`; if explicit path is configured, use it. Extend observability with a file-output constructor or option without changing Control Plane/Node callers. `env` must print the effective log path.

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

- [ ] `packages/config/config.go`: add log path field and tests.
- [ ] `packages/observability/observability.go`: add file logger constructor or option while keeping existing `NewLogger` behavior.
- [ ] `apps/edge/cmd/edge/main.go`: resolve effective log path for `serve`, `console`, and `env`.
- [ ] `apps/edge/internal/bootstrap/runtime.go`: use the effective path when present.
- [ ] `apps/edge/internal/bootstrap/runtime_test.go`: verify runtime accepts file logging without starting metrics.
- [ ] `apps/edge/cmd/edge/main_test.go`: verify clean temp dir creates `<dir>/logs/edge.log` after starting a short-lived command or isolated logger helper.

#### 테스트 작성

Write a focused test around the resolver/logger helper instead of a long-running full `serve` process where possible. Add one integration-style CLI test if helper coverage cannot prove file creation.

#### 중간 검증

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

Expected: pass; cache output is not acceptable.

## 수정 파일 요약

| 파일 | 항목 |
|---|---|
| `apps/edge/cmd/edge/main.go` | CONFIG-1, CONFIG-2, CONFIG-3 |
| `apps/edge/cmd/edge/main_test.go` | CONFIG-1, CONFIG-2, CONFIG-3 |
| `packages/config/config.go` | CONFIG-2, CONFIG-3 |
| `packages/config/config_test.go` | CONFIG-2, CONFIG-3 |
| `packages/observability/observability.go` | CONFIG-3 |
| `apps/edge/internal/bootstrap/runtime.go` | CONFIG-3 |
| `apps/edge/internal/bootstrap/runtime_test.go` | CONFIG-3 |
| `configs/edge.yaml` | CONFIG-2, CONFIG-3 |

## 최종 검증

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

```bash
tmp="$(mktemp -d)" && go build -o "$tmp/iop-edge" ./apps/edge/cmd/edge && (cd "$tmp" && ./iop-edge config init --force && ./iop-edge config check && ./iop-edge env)
```

Expected: tests pass; temp bundle command prints `OK edge.yaml` or the resolved temp path and env output includes config path, binary dir, log path, advertise host, Node transport address, artifact/bootstrap URL, OpenAI-compatible URL, configured nodes, connected nodes.

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