feat(edge): OTO bootstrap 설정 계약을 추가한다

OTO agent bootstrap command 발급 전 Edge config가 OTO script URL과 release base URL을 보존하고 검증할 수 있어야 한다.\n\nRoadmap/task 산출물도 함께 기록해 다음 agent register command 작업이 같은 마일스톤 컨텍스트를 이어받도록 한다.
This commit is contained in:
toki 2026-06-03 19:58:32 +09:00
parent 167dd26c81
commit e5c3a9dabb
13 changed files with 1027 additions and 44 deletions

View file

@ -2,14 +2,14 @@
## 활성 Phase
- [진행중] Control Plane과 Client 운영
- 경로: `agent-roadmap/phase/control-plane-portal-ops/PHASE.md`
- [진행중] Automation Runtime과 Bridge 확장
- 경로: `agent-roadmap/phase/automation-runtime-bridge/PHASE.md`
## 활성 Milestone
- [계획] Multi-Edge 운영
- Phase: `agent-roadmap/phase/control-plane-portal-ops/PHASE.md`
- 경로: `agent-roadmap/phase/control-plane-portal-ops/milestones/multi-edge-operations.md`
- [진행중] OTO Bootstrap Command 발급
- Phase: `agent-roadmap/phase/automation-runtime-bridge/PHASE.md`
- 경로: `agent-roadmap/phase/automation-runtime-bridge/milestones/oto-bootstrap-command-issue.md`
## 선택 규칙

View file

@ -30,7 +30,7 @@ CLI 실행, specialized agent 등록, bootstrap/enrollment, 원격 터미널 브
- 경로: `agent-roadmap/phase/automation-runtime-bridge/milestones/specialized-agent-proto-socket-foundation.md`
- 요약: OTO의 node-compatible proto-socket registration smoke를 바탕으로 Edge의 specialized agent kind, registry/config, message boundary, reconnect 정책과 runtime 경로 구분을 완료 후보로 정리했다.
- [계획] OTO Bootstrap Command 발급
- [진행중] OTO Bootstrap Command 발급
- 경로: `agent-roadmap/phase/automation-runtime-bridge/milestones/oto-bootstrap-command-issue.md`
- 요약: `iop-edge`가 OTO agent record를 만들고 OTO Linux bootstrap script가 소비할 one-line command를 발급하는 CLI/config 표면을 구현한다.

View file

@ -12,7 +12,7 @@ Edge는 OTO bootstrap script를 구현하지 않고, OTO가 소유한 script 인
## 상태
[계획]
[진행중]
## 구현 잠금

View file

@ -0,0 +1,188 @@
<!-- task=m-oto-bootstrap-command-issue/01_oto_bootstrap_config plan=0 tag=CONFIG -->
# Code Review Reference - CONFIG
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Evidence gaps that a follow-up agent can close by rerunning commands or collecting artifacts are normal follow-up issues, not user-review blockers by themselves.
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume.
> Follow the ownership table at the bottom of this file for which sections you own.
## 개요
date=2026-06-03
task=m-oto-bootstrap-command-issue/01_oto_bootstrap_config, plan=0, tag=CONFIG
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
1. 판정을 append한다.
2. `CODE_REVIEW-{review_lane}-GNN.md` -> `code_review_{review_lane}_GNN_N.log`, `PLAN-{build_lane}-GNN.md` -> `plan_{build_lane}_GNN_M.log`로 아카이브한다.
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/{task_name}/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 사용자 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다.
4. PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [CONFIG-1] Edge bootstrap config에 OTO script/release URL 필드를 추가하고 load/default/example 테스트를 작성한다. | [x] |
| [CONFIG-2] YAML patch helper가 `artifact_base_url`, `oto_bootstrap_script_url`, `oto_release_base_url`을 같은 방식으로 보존/갱신하게 만든다. | [x] |
## 구현 체크리스트
- [x] [CONFIG-1] Edge bootstrap config에 OTO script/release URL 필드를 추가하고 load/default/example 테스트를 작성한다.
- [x] [CONFIG-2] YAML patch helper가 `artifact_base_url`, `oto_bootstrap_script_url`, `oto_release_base_url`을 같은 방식으로 보존/갱신하게 만든다.
- [x] [CONFIG-3] `go test -count=1 ./packages/go/config ./apps/edge/cmd/edge`를 실행한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [x] active `CODE_REVIEW-*-G??.md`를 `code_review_{review_lane}_GNN_N.log`로 아카이브한다.
- [x] active `PLAN-*-G??.md`를 `plan_{build_lane}_GNN_M.log`로 아카이브한다.
- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하여 plan/review/archive 산출물이 추적 가능한지 확인한다.
- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [x] PASS이면 active task 디렉터리 `agent-task/m-oto-bootstrap-command-issue/01_oto_bootstrap_config/`를 `agent-task/archive/YYYY/MM/m-oto-bootstrap-command-issue/01_oto_bootstrap_config/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [x] PASS이고 task group이 `m-oto-bootstrap-command-issue`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-oto-bootstrap-command-issue/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-{build_lane}-GNN.md`와 `CODE_REVIEW-{review_lane}-GNN.md`를 작성하고 `complete.log`를 작성하지 않는다.
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
## 계획 대비 변경 사항
없음.
## 주요 설계 결정
- `patchBootstrapStringFields` 헬퍼 함수를 추가하여 `artifact_base_url`, `oto_bootstrap_script_url`, `oto_release_base_url`을 순서대로 일관되고 결정론적(deterministic order)인 방식으로 YAML 노드에 패치하도록 설계했습니다. 이를 통해 map의 순서 무작위성으로 인해 YAML이 불필요하게 변경되는 현상을 막았습니다.
## 사용자 리뷰 요청
- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- `bootstrap.oto_bootstrap_script_url`과 `bootstrap.oto_release_base_url`이 loader/default/example/test에 모두 반영됐는가.
- YAML patch helper가 기존 `artifact_base_url` 동작을 깨지 않고 새 OTO 필드를 보존형으로 추가/갱신하는가.
- 이 subtask가 `agent register` command 구현으로 scope를 넓히지 않았는가.
## 검증 결과
### CONFIG-1 중간 검증
```bash
$ go test -count=1 ./packages/go/config
ok iop/packages/go/config 0.019s
```
### CONFIG-2 중간 검증
```bash
$ go test -count=1 ./apps/edge/cmd/edge
ok iop/apps/edge/cmd/edge 0.054s
```
### 최종 검증
```bash
$ go test -count=1 ./packages/go/config ./apps/edge/cmd/edge
ok iop/packages/go/config 0.018s
ok iop/apps/edge/cmd/edge 0.078s
```
```bash
$ go test -count=1 ./packages/go/... ./apps/edge/...
? iop/packages/go/auth [no test files]
ok iop/packages/go/config 0.021s
? iop/packages/go/events [no test files]
? iop/packages/go/hostsetup [no test files]
? iop/packages/go/jobs [no test files]
? iop/packages/go/metadata [no test files]
? iop/packages/go/observability [no test files]
? iop/packages/go/policy [no test files]
? iop/packages/go/version [no test files]
ok iop/apps/edge/cmd/edge 0.054s
? iop/apps/edge/internal/bootstrap [no test files]
? iop/apps/edge/internal/controlplane [no test files]
? iop/apps/edge/internal/events [no test files]
? iop/apps/edge/internal/input [no test files]
ok iop/apps/edge/internal/node 0.015s
? iop/apps/edge/internal/openai [no test files]
? iop/apps/edge/internal/opsconsole [no test files]
? iop/apps/edge/internal/service [no test files]
ok iop/apps/edge/internal/transport 0.134s
```
---
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
> If anything is blank, go back and fill it in before saving this file.
> Leave review-agent-only sections unchanged.
## 코드리뷰 결과
- 종합 판정: PASS
- 차원별 평가:
- correctness: Pass
- completeness: Pass
- test coverage: Pass
- API contract: Pass
- code quality: Pass
- plan deviation: Pass
- verification trust: Pass
- 발견된 문제: 없음
- 다음 단계: PASS 종결. `complete.log` 작성 후 task archive로 이동한다.
리뷰 검증:
```bash
$ go test -count=1 ./packages/go/config ./apps/edge/cmd/edge
ok iop/packages/go/config 0.049s
ok iop/apps/edge/cmd/edge 0.093s
```
```bash
$ go test -count=1 ./apps/edge/internal/controlplane -run TestConnectorStopClosesConnection -v
=== RUN TestConnectorStopClosesConnection
--- PASS: TestConnectorStopClosesConnection (0.01s)
PASS
ok iop/apps/edge/internal/controlplane 0.018s
```
```bash
$ go test -count=1 ./packages/go/... ./apps/edge/...
ok iop/packages/go/audit 0.005s
ok iop/packages/go/config 0.016s
ok iop/packages/go/hostsetup 0.006s
ok iop/packages/go/observability 0.014s
ok iop/apps/edge/cmd/edge 0.057s
ok iop/apps/edge/internal/bootstrap 0.032s
ok iop/apps/edge/internal/controlplane 4.451s
ok iop/apps/edge/internal/events 0.009s
ok iop/apps/edge/internal/input 0.008s
ok iop/apps/edge/internal/input/a2a 0.008s
ok iop/apps/edge/internal/node 0.015s
ok iop/apps/edge/internal/openai 1.516s
ok iop/apps/edge/internal/opsconsole 0.008s
ok iop/apps/edge/internal/service 0.007s
ok iop/apps/edge/internal/transport 0.018s
```
참고: 첫 `go test -count=1 ./packages/go/... ./apps/edge/...` 재실행에서 이번 변경과 무관한 `apps/edge/internal/controlplane`의 `TestConnectorStopClosesConnection`이 1회 실패했으나, 해당 단일 테스트와 전체 범위 재실행이 이어서 통과했다.

View file

@ -0,0 +1,36 @@
# Complete - m-oto-bootstrap-command-issue/01_oto_bootstrap_config
## 완료 일시
2026-06-03
## 요약
CONFIG plan 0/code-review 0 loop를 PASS로 종결했다. Edge bootstrap OTO URL config 계약과 YAML patch helper 변경이 계획 범위와 검증 기준을 충족했다.
## 루프 이력
| Plan | Review | Verdict | 메모 |
|------|--------|---------|------|
| `plan_local_G05_0.log` | `code_review_local_G05_0.log` | PASS | Required/Suggested 이슈 없음 |
## 구현/정리 내용
- `EdgeBootstrapConf`에 `oto_bootstrap_script_url`, `oto_release_base_url` 필드와 empty default를 추가했다.
- `configs/edge.yaml` bootstrap 예시에 OTO bootstrap script/release base URL 필드를 추가했다.
- `patchBootstrapStringFields` helper로 `artifact_base_url`, `oto_bootstrap_script_url`, `oto_release_base_url` YAML patch 경로를 결정론적으로 통합했다.
- config load/default 테스트와 Edge YAML patch 테스트를 추가했다.
## 최종 검증
- `go test -count=1 ./packages/go/config ./apps/edge/cmd/edge` - PASS; `iop/packages/go/config`, `iop/apps/edge/cmd/edge` 모두 통과.
- `go test -count=1 ./apps/edge/internal/controlplane -run TestConnectorStopClosesConnection -v` - PASS; 첫 전체 범위 실행에서 1회 실패한 무관 control-plane 테스트를 단독 재확인.
- `go test -count=1 ./packages/go/... ./apps/edge/...` - PASS; packages/go 및 apps/edge 범위 재실행 통과.
## 잔여 Nit
- 없음
## 후속 작업
- 없음

View file

@ -0,0 +1,181 @@
<!-- task=m-oto-bootstrap-command-issue/01_oto_bootstrap_config plan=0 tag=CONFIG -->
# Plan - CONFIG
## 이 파일을 읽는 구현 에이전트에게
`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션 작성은 필수다. 구현 후 검증을 실행하고 실제 구현 내용과 stdout/stderr를 review stub에 기록한 뒤 active 파일을 그대로 두고 리뷰 준비 상태를 보고한다. 최종 판정, log rename, `complete.log`, archive 이동은 code-review 전용이다. 사용자 결정, 사용자 소유 외부 환경, scope 충돌로 막히면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 결정과 증거를 남기고 멈춘다. 후속 에이전트가 재실행이나 증거 수집으로 해소할 수 있는 검증 공백은 사용자 리뷰 요청이 아니다.
## 배경
현재 `node register`는 Node artifact URL만 `bootstrap.artifact_base_url`에 보존한다. OTO bootstrap command는 OTO 소유 script URL과 release base URL을 함께 소비해야 하므로 Edge config가 두 값을 읽고 YAML patch가 보존할 수 있어야 한다. 이 선행 작업은 command 구현 전에 config 계약만 작게 고정한다.
## 사용자 리뷰 요청 흐름
구현 중 blocker는 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 섹션 형식은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md`를 따른다. code-review가 요청 정당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.
## 분석 결과
### 읽은 파일
- `agent-roadmap/current.md`
- `agent-roadmap/phase/automation-runtime-bridge/PHASE.md`
- `agent-roadmap/phase/automation-runtime-bridge/milestones/oto-bootstrap-command-issue.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/edge-smoke.md`
- `agent-test/local/platform-common-smoke.md`
- `agent-test/local/testing-smoke.md`
- `apps/edge/cmd/edge/main.go`
- `apps/edge/cmd/edge/main_test.go`
- `packages/go/config/config.go`
- `packages/go/config/config_test.go`
- `configs/edge.yaml`
- `apps/edge/internal/node/mapper.go`
- `apps/edge/internal/node/store.go`
- `apps/edge/internal/transport/server.go`
- `../oto/assets/script/shell/oto_agent_bootstrap.sh`
### 테스트 환경 규칙
- `test_env=local`; `agent-test/local/rules.md`를 읽었다.
- 적용 profile: `agent-test/local/edge-smoke.md`, `agent-test/local/platform-common-smoke.md`, `agent-test/local/testing-smoke.md`.
- 적용 명령: edge 변경은 `go test ./apps/edge/...`, platform-common 변경은 `go test ./packages/go/...`, bootstrap UX 변경은 변경 entrypoint full-cycle 확인을 요구한다.
- 이 subtask의 최종 명령은 fresh 실행을 위해 `-count=1`을 사용한다. fallback 없음.
### 테스트 커버리지 공백
- `bootstrap.oto_bootstrap_script_url`, `bootstrap.oto_release_base_url` 로딩: 기존 테스트 없음. `packages/go/config/config_test.go`에 default/explicit load 테스트를 추가한다.
- YAML patch가 새 bootstrap 필드를 보존/갱신하는 동작: 기존 `artifact_base_url`만 다룬다. `apps/edge/cmd/edge/main_test.go`에 patch roundtrip 테스트를 추가한다.
- OTO agent adapterless config check는 작은 선행 변경으로 `TestConfigCheckAcceptsOTOAgentWithoutAdapters`가 추가되어 통과했다.
### 심볼 참조
- renamed/removed symbol: none.
### 분할 판단
- split decision policy를 plan 파일 선택 전에 평가했다.
- shared task group: `m-oto-bootstrap-command-issue`.
- `01_oto_bootstrap_config`: 독립 선행 작업. config struct/default/example/YAML patch helper를 다룬다.
- `02+01_agent_register_command`: `01` 완료 후 command surface가 새 config 계약을 소비한다.
- 이 plan은 predecessor가 없다. sibling `02+01_agent_register_command`는 이 subtask의 `complete.log`를 기다린다.
### 범위 결정 근거
- 포함: `packages/go/config`, `configs/edge.yaml`, `apps/edge/cmd/edge`의 YAML patch helper와 테스트.
- 제외: `agent register` command 구현, OTO bootstrap script 구현, release asset build/upload, checksum/signature, Control Plane UI.
- `../oto`는 script 인자 계약 확인용으로만 읽고 수정하지 않는다.
### 빌드 등급
- build=`local-G05`, review=`local-G05`. Config 계약과 YAML patch helper 중심의 bounded 변경이고 Go 테스트로 검증 가능하다.
## 구현 체크리스트
- [ ] [CONFIG-1] Edge bootstrap config에 OTO script/release URL 필드를 추가하고 load/default/example 테스트를 작성한다.
- [ ] [CONFIG-2] YAML patch helper가 `artifact_base_url`, `oto_bootstrap_script_url`, `oto_release_base_url`을 같은 방식으로 보존/갱신하게 만든다.
- [ ] [CONFIG-3] `go test -count=1 ./packages/go/config ./apps/edge/cmd/edge`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## CONFIG-1 Edge bootstrap config 필드
### 문제
`packages/go/config/config.go:86`의 `EdgeBootstrapConf`는 Node artifact URL만 가진다.
```go
86 type EdgeBootstrapConf struct {
87 ArtifactBaseURL string `mapstructure:"artifact_base_url" yaml:"artifact_base_url"`
88 Listen string `mapstructure:"listen" yaml:"listen"`
89 ArtifactDir string `mapstructure:"artifact_dir" yaml:"artifact_dir"`
90 }
```
OTO script는 필수 `--release-base-url`을 요구하고 HTTPS를 검증한다: `../oto/assets/script/shell/oto_agent_bootstrap.sh:16`, `../oto/assets/script/shell/oto_agent_bootstrap.sh:82`.
### 해결 방법
`EdgeBootstrapConf`에 아래 필드를 추가한다.
```go
OTOBootstrapScriptURL string `mapstructure:"oto_bootstrap_script_url" yaml:"oto_bootstrap_script_url"`
OTOReleaseBaseURL string `mapstructure:"oto_release_base_url" yaml:"oto_release_base_url"`
```
`setEdgeDefaults`에는 두 값을 빈 문자열 default로 둔다. `configs/edge.yaml`의 `bootstrap:` 주석과 예시에 두 필드를 추가한다.
### 수정 파일 및 체크리스트
- [ ] `packages/go/config/config.go`에 필드와 defaults를 추가한다.
- [ ] `packages/go/config/config_test.go`에 default empty와 explicit load 테스트를 추가한다.
- [ ] `configs/edge.yaml`의 `bootstrap:` 예시에 두 값을 빈 문자열로 추가한다.
### 테스트 작성
작성한다. `packages/go/config/config_test.go`에 `TestLoadEdge_OTOBootstrapDefaults`와 `TestLoadEdge_OTOBootstrapExplicit`를 추가한다. 목표는 default empty와 YAML explicit 값이 loader/runtime load에서 유지되는지 확인하는 것이다.
### 중간 검증
```bash
go test -count=1 ./packages/go/config
```
기대 결과: `ok iop/packages/go/config`.
## CONFIG-2 YAML patch helper 확장
### 문제
`apps/edge/cmd/edge/main.go:1163` 이후 `patchYAML`은 `bootstrap.artifact_base_url` 하나만 patch한다. 새 command가 OTO URL을 config에 기록하려면 같은 보존형 patch 경로가 필요하다.
```go
1163 // 2. Patch "bootstrap.artifact_base_url" if bootstrap config has artifact URL
1164 if cfg.Bootstrap.ArtifactBaseURL != "" {
...
1192 urlKey := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: "artifact_base_url"}
1193 urlVal := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: cfg.Bootstrap.ArtifactBaseURL}
```
### 해결 방법
`patchBootstrapStringFields(rootMap, map[string]string)` helper를 추가하고 `artifact_base_url`, `oto_bootstrap_script_url`, `oto_release_base_url` 중 값이 비어 있지 않은 필드만 patch한다. 기존 `artifact_base_url` 동작은 유지한다.
### 수정 파일 및 체크리스트
- [ ] `apps/edge/cmd/edge/main.go`에서 bootstrap patch 로직을 helper로 분리한다.
- [ ] `apps/edge/cmd/edge/main_test.go`에 OTO bootstrap fields patch 테스트를 추가한다.
### 테스트 작성
작성한다. `apps/edge/cmd/edge/main_test.go`에 `TestPatchYAMLUpdatesOTOBootstrapFields`를 추가한다. 목표는 기존 YAML layout을 보존하면서 `bootstrap.oto_bootstrap_script_url`과 `bootstrap.oto_release_base_url`이 추가/갱신되는지 확인하는 것이다.
### 중간 검증
```bash
go test -count=1 ./apps/edge/cmd/edge
```
기대 결과: `ok iop/apps/edge/cmd/edge`.
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `packages/go/config/config.go` | CONFIG-1 |
| `packages/go/config/config_test.go` | CONFIG-1 |
| `configs/edge.yaml` | CONFIG-1 |
| `apps/edge/cmd/edge/main.go` | CONFIG-2 |
| `apps/edge/cmd/edge/main_test.go` | CONFIG-2 |
## 최종 검증
```bash
go test -count=1 ./packages/go/config ./apps/edge/cmd/edge
go test -count=1 ./packages/go/... ./apps/edge/...
```
기대 결과: 모든 package가 exit code 0으로 종료한다.
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -0,0 +1,160 @@
<!-- task=m-oto-bootstrap-command-issue/02+01_agent_register_command plan=0 tag=CLI -->
# Code Review Reference - CLI
> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Complete the `구현 체크리스트`; the final checklist item is mandatory before saving.
> Fill implementation-owned sections, then stop with active files in place and report ready for review.
> If implementation is blocked by a user-only decision, user-owned external environment prerequisite, or scope conflict, fill `사용자 리뷰 요청` with evidence and stop with active files in place; code-review decides whether to write `USER_REVIEW.md`. Evidence gaps that a follow-up agent can close by rerunning commands or collecting artifacts are normal follow-up issues, not user-review blockers by themselves.
> Finalization (`코드리뷰 결과`, log rename, `complete.log`, archive moves, `코드리뷰 전용 체크리스트`) is review-agent-only, even after compaction/resume.
> Follow the ownership table at the bottom of this file for which sections you own.
## 개요
date=2026-06-03
task=m-oto-bootstrap-command-issue/02+01_agent_register_command, plan=0, tag=CLI
## Roadmap Targets
- Milestone: `agent-roadmap/phase/automation-runtime-bridge/milestones/oto-bootstrap-command-issue.md`
- Task ids:
- `agent-register-cmd`: `iop-edge`가 OTO agent record를 만들거나 갱신하는 command 표면을 제공한다.
- `node-interface-parity`: OTO agent command/config 표면이 기존 `iop-node` bootstrap의 필드명, flag 의미, 출력 형식, 진단 흐름을 최대한 유지한다.
- `bootstrap-output`: command 출력은 한 줄 bootstrap command만 안정적으로 제공하고 token을 불필요하게 재출력하지 않는다.
- `oto-args`: 출력 command가 OTO bootstrap 필수 인자 `--edge-url`, `--agent-id`, `--enrollment-token`, `--release-base-url`을 포함한다.
- `config-roundtrip`: 생성된 edge config를 `config check`와 runtime load가 읽을 수 있다.
- Completion mode: check-on-pass
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
1. 판정을 append한다.
2. `CODE_REVIEW-{review_lane}-GNN.md` -> `code_review_{review_lane}_GNN_N.log`, `PLAN-{build_lane}-GNN.md` -> `plan_{build_lane}_GNN_M.log`로 아카이브한다.
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/{task_name}/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다. `USER_REVIEW.md`가 사용자 결정으로 완료/PASS 해소되면 code-review가 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log` 작성 후 archive 이동한다.
4. PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. roadmap 상태 체크와 `update-roadmap` 호출은 런타임 책임이다.
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [CLI-1] `iop-edge agent register [agent-id]` command를 추가하고 OTO agent record를 upsert한다. | [ ] |
| [CLI-2] OTO bootstrap URL/release URL/edge URL 정책과 one-line command renderer를 구현한다. | [ ] |
| [CLI-3] command help, output, config roundtrip, URL boundary, full-cycle command 검증 테스트를 작성한다. | [ ] |
## 구현 체크리스트
- [ ] [CLI-1] `iop-edge agent register [agent-id]` command를 추가하고 OTO agent record를 upsert한다.
- [ ] [CLI-2] OTO bootstrap URL/release URL/edge URL 정책과 one-line command renderer를 구현한다.
- [ ] [CLI-3] command help, output, config roundtrip, URL boundary, full-cycle command 검증 테스트를 작성한다.
- [ ] [CLI-4] `go test -count=1 ./apps/edge/cmd/edge ./packages/go/config`와 실제 `go run ./apps/edge/cmd/edge ... agent register ...` full-cycle command를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## 코드리뷰 전용 체크리스트
> **[REVIEW AGENT ONLY]** 이 체크리스트는 코드리뷰 에이전트만 사용한다.
> 구현 에이전트는 이 섹션을 수정하거나 체크하지 않는다.
- [ ] `코드리뷰 결과``PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [ ] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [ ] active `CODE_REVIEW-*-G??.md``code_review_{review_lane}_GNN_N.log`로 아카이브한다.
- [ ] active `PLAN-*-G??.md``plan_{build_lane}_GNN_M.log`로 아카이브한다.
- [ ] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md``agent-task/**/*.log`를 unignore하여 plan/review/archive 산출물이 추적 가능한지 확인한다.
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [ ] PASS이면 active task 디렉터리 `agent-task/m-oto-bootstrap-command-issue/02+01_agent_register_command/``agent-task/archive/YYYY/MM/m-oto-bootstrap-command-issue/02+01_agent_register_command/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [ ] PASS이고 task group이 `m-oto-bootstrap-command-issue`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-oto-bootstrap-command-issue/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
- [ ] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-{build_lane}-GNN.md``CODE_REVIEW-{review_lane}-GNN.md`를 작성하고 `complete.log`를 작성하지 않는다.
- [ ] USER_REVIEW이면 `agent-ops/skills/common/code-review/templates/user-review-template.md` 기준으로 `USER_REVIEW.md`를 작성하고 active `PLAN-*.md`, `CODE_REVIEW-*.md`, `complete.log`를 남기지 않는다.
- [ ] USER_REVIEW가 사용자 결정으로 완료/PASS 해소되면 `USER_REVIEW.md`를 해소 상태로 갱신하고 `complete.log`를 작성한 뒤 task directory를 archive로 이동한다.
## 계획 대비 변경 사항
_구현 에이전트가 계획과 다르게 구현한 부분을 이유와 함께 기록한다._
## 주요 설계 결정
_구현 에이전트가 주요 설계 결정 사항을 기록한다._
## 사용자 리뷰 요청
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._
- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- `agent-task/m-oto-bootstrap-command-issue/01_oto_bootstrap_config/complete.log`가 존재한 뒤 구현됐는가.
- `agent register`가 OTO record를 `agent_kind=oto-agent`로 저장하고 generic node path를 깨지 않는가.
- 출력이 정확히 한 줄이고 OTO 필수 인자 4개를 포함하며 token을 추가로 출력하지 않는가.
- HTTPS 기본 정책과 explicit local/dev HTTP override가 테스트로 고정됐는가.
- `Roadmap Targets`의 5개 task id가 PASS 근거와 일치하는가.
## 검증 결과
_구현 에이전트가 각 중간 검증 및 최종 검증 명령 실행 후 출력을 여기에 붙여 넣는다._
필수 규칙:
- 검증 명령은 고정된 계약이다. 임의로 대체하지 않는다.
- 대체가 필요하면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
- `검증 결과`에는 실제 stdout/stderr를 붙여 넣는다.
- 사용자 리뷰 요청으로 명령을 끝까지 실행하지 못했다면 `사용자 리뷰 요청`에 실행한 명령, 실제 출력, 미실행 명령의 사유를 기록한다.
- mobile/UI hang, timeout, 또는 2분 무진행은 blind retry를 중단하고 focused rerun 명령과 screenshot/window/UI-tree evidence path를 남기며, 불가능하면 정확한 사유를 남긴다.
### CLI-1 중간 검증
```bash
$ go test -count=1 ./apps/edge/cmd/edge
(output)
```
### CLI-2 중간 검증
```bash
$ go test -count=1 ./apps/edge/cmd/edge
(output)
```
### CLI-3 중간 검증
```bash
$ go test -count=1 ./apps/edge/cmd/edge ./packages/go/config
(output)
```
### 최종 검증
```bash
$ go test -count=1 ./apps/edge/cmd/edge ./packages/go/config
(output)
```
```bash
$ go test -count=1 ./apps/edge/... ./packages/go/...
(output)
```
```bash
$ tmpdir="$(mktemp -d)"; printf 'server:\n listen: "127.0.0.1:19090"\n advertise_host: "edge.example.test"\nbootstrap:\n oto_bootstrap_script_url: "https://oto.example.test/bootstrap/oto_agent_bootstrap.sh"\n oto_release_base_url: "https://oto.example.test/releases"\nnodes: []\n' > "$tmpdir/edge.yaml"; go run ./apps/edge/cmd/edge --config "$tmpdir/edge.yaml" agent register oto-agent-a --token test-token; go run ./apps/edge/cmd/edge --config "$tmpdir/edge.yaml" config check
(output)
```
```bash
$ rg --sort path -n "agent register|oto_bootstrap_script_url|oto_release_base_url|--edge-url|--release-base-url" apps/edge/cmd/edge packages/go/config configs agent-roadmap/phase/automation-runtime-bridge/milestones/oto-bootstrap-command-issue.md
(output)
```
---
> **[IMPLEMENTING AGENT — BEFORE SAVING] Have you filled in every implementation-owned section: completion table, implementation checklist, changes from plan, design decisions, and verification output?**
> If anything is blank, go back and fill it in before saving this file.
> Leave review-agent-only sections unchanged.

View file

@ -0,0 +1,267 @@
<!-- task=m-oto-bootstrap-command-issue/02+01_agent_register_command plan=0 tag=CLI -->
# Plan - CLI
## 이 파일을 읽는 구현 에이전트에게
`CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션 작성은 필수다. 구현 후 검증을 실행하고 실제 구현 내용과 stdout/stderr를 review stub에 기록한 뒤 active 파일을 그대로 두고 리뷰 준비 상태를 보고한다. 최종 판정, log rename, `complete.log`, archive 이동은 code-review 전용이다. 사용자 결정, 사용자 소유 외부 환경, scope 충돌로 막히면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 결정과 증거를 남기고 멈춘다. 후속 에이전트가 재실행이나 증거 수집으로 해소할 수 있는 검증 공백은 사용자 리뷰 요청이 아니다.
## 배경
현재 Edge CLI는 generic node용 `node register`만 제공한다. OTO는 같은 token 기반 proto-socket registration path를 재사용하지만 bootstrap command는 OTO script와 release URL 인자를 소비해야 한다. 이 작업은 선행 config 계약이 완료된 뒤 Edge-local `agent register` command를 추가해 OTO agent record와 one-line bootstrap command를 만든다.
## 사용자 리뷰 요청 흐름
구현 중 blocker는 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/automation-runtime-bridge/milestones/oto-bootstrap-command-issue.md`
- Task ids:
- `agent-register-cmd`: `iop-edge`가 OTO agent record를 만들거나 갱신하는 command 표면을 제공한다.
- `node-interface-parity`: OTO agent command/config 표면이 기존 `iop-node` bootstrap의 필드명, flag 의미, 출력 형식, 진단 흐름을 최대한 유지한다.
- `bootstrap-output`: command 출력은 한 줄 bootstrap command만 안정적으로 제공하고 token을 불필요하게 재출력하지 않는다.
- `oto-args`: 출력 command가 OTO bootstrap 필수 인자 `--edge-url`, `--agent-id`, `--enrollment-token`, `--release-base-url`을 포함한다.
- `config-roundtrip`: 생성된 edge config를 `config check`와 runtime load가 읽을 수 있다.
- Completion mode: check-on-pass
## 분석 결과
### 읽은 파일
- `agent-roadmap/current.md`
- `agent-roadmap/phase/automation-runtime-bridge/PHASE.md`
- `agent-roadmap/phase/automation-runtime-bridge/milestones/oto-bootstrap-command-issue.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/edge-smoke.md`
- `agent-test/local/platform-common-smoke.md`
- `agent-test/local/testing-smoke.md`
- `apps/edge/cmd/edge/main.go`
- `apps/edge/cmd/edge/main_test.go`
- `packages/go/config/config.go`
- `packages/go/config/config_test.go`
- `configs/edge.yaml`
- `apps/edge/internal/node/mapper.go`
- `apps/edge/internal/node/store.go`
- `apps/edge/internal/transport/server.go`
- `../oto/assets/script/shell/oto_agent_bootstrap.sh`
- `../oto/lib/oto/agent/edge_registration_client.dart`
- `../oto/test/oto_agent_config_test.dart`
### 테스트 환경 규칙
- `test_env=local`; `agent-test/local/rules.md`를 읽었다.
- 적용 profile: `agent-test/local/edge-smoke.md`, `agent-test/local/platform-common-smoke.md`, `agent-test/local/testing-smoke.md`.
- 적용 명령: `go test ./apps/edge/...`, `go test ./packages/go/...`, bootstrap/install UX 변경의 실제 entrypoint full-cycle command.
- 이 plan의 Go 검증은 fresh 실행을 위해 `-count=1`을 사용한다. fallback 없음.
### 테스트 커버리지 공백
- OTO `agent register` command: 기존 테스트 없음. 새 CLI tests가 필요하다.
- OTO bootstrap one-line output: 기존 node output tests만 있다. 새 exact-output test가 필요하다.
- OTO 필수 인자 포함: 기존 테스트 없음. command output assertion으로 검증한다.
- HTTP script URL gating: 기존 테스트 없음. boundary test가 필요하다.
- Config roundtrip: 작은 선행 변경으로 adapterless OTO config check는 통과했고, 이 plan은 generated config가 `config check``config.LoadEdge`를 통과하는 테스트를 추가해야 한다.
### 심볼 참조
- renamed/removed symbol: none.
### 분할 판단
- split decision policy를 plan 파일 선택 전에 평가했다.
- shared task group: `m-oto-bootstrap-command-issue`.
- sibling subtasks:
- `01_oto_bootstrap_config`: config struct/default/example/YAML patch 선행 계약.
- `02+01_agent_register_command`: 이 plan. command surface가 `01`의 config fields와 patch helper를 소비한다.
- predecessor `01`: active plan exists at `agent-task/m-oto-bootstrap-command-issue/01_oto_bootstrap_config/PLAN-local-G05.md`; `complete.log` is not present yet. Runtime must wait for predecessor completion before implementing this task.
### 범위 결정 근거
- 포함: `apps/edge/cmd/edge` command surface, CLI tests, generated temp config full-cycle check.
- 제외: OTO bootstrap script 변경, OTO release asset build/upload, checksum/signature enforcement, Control Plane remote issue UI, OTO online smoke. Online smoke는 후속 Milestone이다.
- `../oto`는 script and config parser contract 확인용으로만 읽고 수정하지 않는다.
### 빌드 등급
- build=`cloud-G07`, review=`cloud-G07`. Public CLI workflow, one-line bootstrap output, URL policy, config mutation, full-cycle command evidence가 함께 걸려 terminal/CLI reasoning과 verification trust가 중요하다.
## 의존 관계 및 구현 순서
`02+01_agent_register_command`는 디렉터리 이름 기준으로 predecessor index `01`에 의존한다. `agent-task/m-oto-bootstrap-command-issue/01_oto_bootstrap_config/complete.log`가 생긴 뒤 구현한다. 추가 dependency는 없다.
## 구현 체크리스트
- [ ] [CLI-1] `iop-edge agent register [agent-id]` command를 추가하고 OTO agent record를 upsert한다.
- [ ] [CLI-2] OTO bootstrap URL/release URL/edge URL 정책과 one-line command renderer를 구현한다.
- [ ] [CLI-3] command help, output, config roundtrip, URL boundary, full-cycle command 검증 테스트를 작성한다.
- [ ] [CLI-4] `go test -count=1 ./apps/edge/cmd/edge ./packages/go/config`와 실제 `go run ./apps/edge/cmd/edge ... agent register ...` full-cycle command를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
## CLI-1 Agent register command surface
### 문제
`apps/edge/cmd/edge/main.go:66`의 root command는 `nodeCmd()`만 등록하고 specialized agent command가 없다.
```go
66 root.AddCommand(serveCmd(), consoleCmd(), versionCmd(), configCmd(), envCmd(), setupCmd(), bootstrapCmd(), nodeCmd(), nodesCmd(), smokeCmd())
```
`nodeRegisterCmd`는 generic node adapter/runtime 설정을 전제로 한다: `apps/edge/cmd/edge/main.go:913`.
### 해결 방법
`agentCmd()``agentRegisterCmd()`를 추가하고 root에 `agentCmd()`를 등록한다. Command shape:
```text
iop-edge agent register <agent-id> --bootstrap-script-url <url> --release-base-url <url>
```
Flags:
- `--alias` default `<agent-id>`
- `--token` default generated UUID, same meaning as `node register --token`
- `--edge-url` default `addressFor(resolveAdvertiseHost(cfg.Server.AdvertiseHost).Host, cfg.Server.Listen)`
- `--bootstrap-script-url` overrides and persists `bootstrap.oto_bootstrap_script_url`
- `--release-base-url` overrides and persists `bootstrap.oto_release_base_url`
- `--allow-insecure-bootstrap-url` allows local/dev `http://` script URL only when explicit
Upsert record:
```go
config.NodeDefinition{
ID: agentID,
Alias: alias,
Token: token,
AgentKind: config.AgentKindOTOAgent,
}
```
Do not enable Node adapters for OTO records.
### 수정 파일 및 체크리스트
- [ ] `apps/edge/cmd/edge/main.go``agentCmd`, `agentRegisterCmd`, OTO-specific flag variables를 추가한다.
- [ ] `apps/edge/cmd/edge/main.go` root command에 `agentCmd()`를 등록한다.
- [ ] Existing `node register` behavior는 변경하지 않는다.
### 테스트 작성
작성한다. `apps/edge/cmd/edge/main_test.go``TestAgentRegisterUpsertsOTOAgentRecord`를 추가한다. 목표는 OTO record가 `agent_kind: oto-agent`, alias, token을 보존하고 adapter 없이 config check/load를 통과하는지 확인하는 것이다.
### 중간 검증
```bash
go test -count=1 ./apps/edge/cmd/edge
```
기대 결과: `ok iop/apps/edge/cmd/edge`.
## CLI-2 One-line OTO bootstrap renderer and URL policy
### 문제
기존 `node register` 출력은 Node script와 positional token만 렌더링한다: `apps/edge/cmd/edge/main.go:1101`.
```go
1101 bootstrapURL := universalBootstrapScriptURL(artifactURL)
1102 fmt.Fprintf(cmd.OutOrStdout(), "curl -fsSL %s | bash -s %s\n", bootstrapURL, nodeDef.Token)
```
OTO script는 named 필수 인자 4개를 요구한다: `../oto/assets/script/shell/oto_agent_bootstrap.sh:16`, `../oto/assets/script/shell/oto_agent_bootstrap.sh:76`.
### 해결 방법
`renderOTOBootstrapCommand(opts)` helper를 추가한다. Output must be exactly one line and include:
```text
curl -fsSL <bootstrap-script-url> | bash -s -- --edge-url <edge-url> --agent-id <agent-id> --enrollment-token <token> --release-base-url <release-base-url> --agent-alias <alias>
```
Validation:
- script URL and release base URL must come from flag or config; missing values fail before YAML write.
- `release-base-url` must start with `https://`.
- script URL must start with `https://` unless `--allow-insecure-bootstrap-url` is set. `http://` is accepted only with that explicit flag.
- token appears only as the `--enrollment-token` value in the one-line command.
### 수정 파일 및 체크리스트
- [ ] `apps/edge/cmd/edge/main.go`에 OTO URL resolver/validator를 추가한다.
- [ ] `apps/edge/cmd/edge/main.go`에 one-line renderer를 추가한다.
- [ ] `apps/edge/cmd/edge/main_test.go`에 exact output and URL boundary tests를 추가한다.
### 테스트 작성
작성한다. Tests:
- `TestAgentRegisterPrintsOneLineOTOCommand`
- `TestAgentRegisterRejectsInsecureBootstrapURLWithoutFlag`
- `TestAgentRegisterRejectsInsecureReleaseBaseURL`
- `TestAgentRegisterAllowsInsecureBootstrapURLWithExplicitFlag`
### 중간 검증
```bash
go test -count=1 ./apps/edge/cmd/edge
```
기대 결과: `ok iop/apps/edge/cmd/edge`.
## CLI-3 Help, config roundtrip, and full-cycle command evidence
### 문제
Root help currently advertises only `node register`: `apps/edge/cmd/edge/main.go:49`, and help tests assert the same at `apps/edge/cmd/edge/main_test.go:1099`. The milestone requires OTO agent command/config parity and generated config roundtrip.
### 해결 방법
Update help without replacing the existing generic node flow. Add OTO agent register as an optional specialized agent path. Tests must assert command discovery and no legacy script path leakage.
Generated config roundtrip test should run command against a temp `edge.yaml`, then:
- parse YAML and assert OTO URL config fields are persisted,
- call `config.LoadEdge`,
- run `config check`,
- assert output is exactly one command line.
### 수정 파일 및 체크리스트
- [ ] `apps/edge/cmd/edge/main.go` root/help examples mention `agent register` as OTO specialized path.
- [ ] `apps/edge/cmd/edge/main_test.go` updates command discovery/help tests.
- [ ] `apps/edge/cmd/edge/main_test.go` adds generated config roundtrip test.
### 테스트 작성
작성한다. `TestHelpDiscoversEdgeLocalDevFlow``agent register` command discovery를 추가하고, a full roundtrip test를 새로 추가한다.
### 중간 검증
```bash
go test -count=1 ./apps/edge/cmd/edge ./packages/go/config
```
기대 결과: both packages pass.
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `apps/edge/cmd/edge/main.go` | CLI-1, CLI-2, CLI-3 |
| `apps/edge/cmd/edge/main_test.go` | CLI-1, CLI-2, CLI-3 |
## 최종 검증
```bash
go test -count=1 ./apps/edge/cmd/edge ./packages/go/config
go test -count=1 ./apps/edge/... ./packages/go/...
tmpdir="$(mktemp -d)"; printf 'server:\n listen: "127.0.0.1:19090"\n advertise_host: "edge.example.test"\nbootstrap:\n oto_bootstrap_script_url: "https://oto.example.test/bootstrap/oto_agent_bootstrap.sh"\n oto_release_base_url: "https://oto.example.test/releases"\nnodes: []\n' > "$tmpdir/edge.yaml"; go run ./apps/edge/cmd/edge --config "$tmpdir/edge.yaml" agent register oto-agent-a --token test-token; go run ./apps/edge/cmd/edge --config "$tmpdir/edge.yaml" config check
rg --sort path -n "agent register|oto_bootstrap_script_url|oto_release_base_url|--edge-url|--release-base-url" apps/edge/cmd/edge packages/go/config configs agent-roadmap/phase/automation-runtime-bridge/milestones/oto-bootstrap-command-issue.md
```
기대 결과: Go tests pass; full-cycle command prints one bootstrap line then config check prints `OK`; search output shows command/config/milestone anchors.
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -880,7 +880,7 @@ func validateEdgeConfig(cfg *config.EdgeConfig) error {
name = fmt.Sprintf("index %d", i)
}
if !n.Adapters.Ollama.Enabled && !n.Adapters.CLI.Enabled && !n.Adapters.Vllm.Enabled {
if n.AgentKind != config.AgentKindOTOAgent && !n.Adapters.Ollama.Enabled && !n.Adapters.CLI.Enabled && !n.Adapters.Vllm.Enabled {
return fmt.Errorf("node %q: at least one adapter must be enabled", name)
}
if n.Adapters.Ollama.Enabled && n.Adapters.Ollama.BaseURL == "" {
@ -1160,41 +1160,18 @@ func patchYAML(data []byte, cfg *config.EdgeConfig) ([]byte, error) {
rootMap.Content = append(rootMap.Content, keyNode, nodesValNode)
}
// 2. Patch "bootstrap.artifact_base_url" if bootstrap config has artifact URL
// 2. Patch bootstrap fields if any are present in the config
fieldsToPatch := make(map[string]string)
if cfg.Bootstrap.ArtifactBaseURL != "" {
foundBootstrap := false
for i := 0; i < len(rootMap.Content); i += 2 {
if rootMap.Content[i].Value == "bootstrap" {
bootstrapVal := rootMap.Content[i+1]
if bootstrapVal.Kind == yaml.MappingNode {
foundURL := false
for j := 0; j < len(bootstrapVal.Content); j += 2 {
if bootstrapVal.Content[j].Value == "artifact_base_url" {
bootstrapVal.Content[j+1].Value = cfg.Bootstrap.ArtifactBaseURL
bootstrapVal.Content[j+1].Tag = "!!str"
foundURL = true
break
}
}
if !foundURL {
urlKey := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: "artifact_base_url"}
urlVal := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: cfg.Bootstrap.ArtifactBaseURL}
bootstrapVal.Content = append(bootstrapVal.Content, urlKey, urlVal)
}
}
foundBootstrap = true
break
}
}
if !foundBootstrap {
bootstrapKey := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: "bootstrap"}
bootstrapVal := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}
urlKey := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: "artifact_base_url"}
urlVal := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: cfg.Bootstrap.ArtifactBaseURL}
bootstrapVal.Content = append(bootstrapVal.Content, urlKey, urlVal)
rootMap.Content = append(rootMap.Content, bootstrapKey, bootstrapVal)
}
fieldsToPatch["artifact_base_url"] = cfg.Bootstrap.ArtifactBaseURL
}
if cfg.Bootstrap.OTOBootstrapScriptURL != "" {
fieldsToPatch["oto_bootstrap_script_url"] = cfg.Bootstrap.OTOBootstrapScriptURL
}
if cfg.Bootstrap.OTOReleaseBaseURL != "" {
fieldsToPatch["oto_release_base_url"] = cfg.Bootstrap.OTOReleaseBaseURL
}
patchBootstrapStringFields(rootMap, fieldsToPatch)
out, err := yaml.Marshal(&doc)
if err != nil {
@ -1203,6 +1180,55 @@ func patchYAML(data []byte, cfg *config.EdgeConfig) ([]byte, error) {
return out, nil
}
func patchBootstrapStringFields(rootMap *yaml.Node, fields map[string]string) {
if len(fields) == 0 {
return
}
var bootstrapVal *yaml.Node
foundBootstrap := false
for i := 0; i < len(rootMap.Content); i += 2 {
if rootMap.Content[i].Value == "bootstrap" {
bootstrapVal = rootMap.Content[i+1]
foundBootstrap = true
break
}
}
if !foundBootstrap {
bootstrapKey := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: "bootstrap"}
bootstrapVal = &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}
rootMap.Content = append(rootMap.Content, bootstrapKey, bootstrapVal)
}
if bootstrapVal.Kind != yaml.MappingNode {
return
}
// Iterate in a deterministic order
keys := []string{"artifact_base_url", "oto_bootstrap_script_url", "oto_release_base_url"}
for _, k := range keys {
v, ok := fields[k]
if !ok || v == "" {
continue
}
foundField := false
for j := 0; j < len(bootstrapVal.Content); j += 2 {
if bootstrapVal.Content[j].Value == k {
bootstrapVal.Content[j+1].Value = v
bootstrapVal.Content[j+1].Tag = "!!str"
foundField = true
break
}
}
if !foundField {
keyNode := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: k}
valNode := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: v}
bootstrapVal.Content = append(bootstrapVal.Content, keyNode, valNode)
}
}
}
func nodesCmd() *cobra.Command {
c := &cobra.Command{
Use: "nodes",

View file

@ -639,6 +639,35 @@ nodes:
}
}
func TestConfigCheckAcceptsOTOAgentWithoutAdapters(t *testing.T) {
dir := t.TempDir()
cfgPath := filepath.Join(dir, "edge.yaml")
yamlStr := `server:
listen: "0.0.0.0:9090"
nodes:
- id: "oto-agent-a"
alias: "oto-a"
token: "oto-token-a"
agent_kind: "oto-agent"
`
if err := os.WriteFile(cfgPath, []byte(yamlStr), 0o600); err != nil {
t.Fatalf("write yaml: %v", err)
}
root := rootCmd()
var out bytes.Buffer
root.SetOut(&out)
root.SetErr(&out)
root.SetArgs([]string{"config", "check", "--config", cfgPath})
if err := root.Execute(); err != nil {
t.Fatalf("expected OTO agent config to check without adapters: %v\n%s", err, out.String())
}
if !strings.Contains(out.String(), "OK "+cfgPath) {
t.Fatalf("expected config check OK for OTO agent config, got %q", out.String())
}
}
func TestNodeRegisterUpsertsYAMLAndOutputsBootstrap(t *testing.T) {
// Mock token generator
origGen := tokenGenerator
@ -1304,3 +1333,51 @@ func TestSmokeOpenAICommandFailure(t *testing.T) {
t.Errorf("unexpected error message: %v", err3)
}
}
func TestPatchYAMLUpdatesOTOBootstrapFields(t *testing.T) {
origYAML := `server:
listen: "0.0.0.0:9090"
bootstrap:
artifact_base_url: "http://example.com/artifacts"
`
cfg := &config.EdgeConfig{
Bootstrap: config.EdgeBootstrapConf{
ArtifactBaseURL: "http://example.com/artifacts",
OTOBootstrapScriptURL: "https://example.com/bootstrap.sh",
OTOReleaseBaseURL: "https://example.com/releases",
},
}
patched, err := patchYAML([]byte(origYAML), cfg)
if err != nil {
t.Fatalf("patchYAML: %v", err)
}
var doc struct {
Server struct {
Listen string `yaml:"listen"`
} `yaml:"server"`
Bootstrap struct {
ArtifactBaseURL string `yaml:"artifact_base_url"`
OTOBootstrapScriptURL string `yaml:"oto_bootstrap_script_url"`
OTOReleaseBaseURL string `yaml:"oto_release_base_url"`
} `yaml:"bootstrap"`
}
if err := yaml.Unmarshal(patched, &doc); err != nil {
t.Fatalf("unmarshal patched: %v", err)
}
if doc.Server.Listen != "0.0.0.0:9090" {
t.Errorf("server.listen was modified: %q", doc.Server.Listen)
}
if doc.Bootstrap.ArtifactBaseURL != "http://example.com/artifacts" {
t.Errorf("artifact_base_url = %q", doc.Bootstrap.ArtifactBaseURL)
}
if doc.Bootstrap.OTOBootstrapScriptURL != "https://example.com/bootstrap.sh" {
t.Errorf("oto_bootstrap_script_url = %q", doc.Bootstrap.OTOBootstrapScriptURL)
}
if doc.Bootstrap.OTOReleaseBaseURL != "https://example.com/releases" {
t.Errorf("oto_release_base_url = %q", doc.Bootstrap.OTOReleaseBaseURL)
}
}

View file

@ -13,6 +13,8 @@ server:
# field channel is provisioned.
bootstrap:
artifact_base_url: ""
oto_bootstrap_script_url: ""
oto_release_base_url: ""
tls:
enabled: false

View file

@ -84,9 +84,11 @@ type EdgeServerConf struct {
}
type EdgeBootstrapConf struct {
ArtifactBaseURL string `mapstructure:"artifact_base_url" yaml:"artifact_base_url"`
Listen string `mapstructure:"listen" yaml:"listen"`
ArtifactDir string `mapstructure:"artifact_dir" yaml:"artifact_dir"`
ArtifactBaseURL string `mapstructure:"artifact_base_url" yaml:"artifact_base_url"`
Listen string `mapstructure:"listen" yaml:"listen"`
ArtifactDir string `mapstructure:"artifact_dir" yaml:"artifact_dir"`
OTOBootstrapScriptURL string `mapstructure:"oto_bootstrap_script_url" yaml:"oto_bootstrap_script_url"`
OTOReleaseBaseURL string `mapstructure:"oto_release_base_url" yaml:"oto_release_base_url"`
}
type EdgeOpenAIConf struct {
@ -267,6 +269,8 @@ func setEdgeDefaults(v *viper.Viper) {
v.SetDefault("server.listen", "0.0.0.0:9090")
v.SetDefault("bootstrap.listen", "0.0.0.0:18080")
v.SetDefault("bootstrap.artifact_dir", "artifacts")
v.SetDefault("bootstrap.oto_bootstrap_script_url", "")
v.SetDefault("bootstrap.oto_release_base_url", "")
v.SetDefault("openai.enabled", false)
v.SetDefault("openai.listen", "0.0.0.0:8080")
v.SetDefault("openai.adapter", "ollama")

View file

@ -722,6 +722,48 @@ logging:
}
}
func TestLoadEdge_OTOBootstrapDefaults(t *testing.T) {
dir := t.TempDir()
f := filepath.Join(dir, "edge.yaml")
if err := os.WriteFile(f, []byte("server:\n listen: \"0.0.0.0:9090\"\n"), 0o600); err != nil {
t.Fatalf("write yaml: %v", err)
}
cfg, err := config.LoadEdge(f)
if err != nil {
t.Fatalf("load: %v", err)
}
if cfg.Bootstrap.OTOBootstrapScriptURL != "" {
t.Errorf("expected empty bootstrap.oto_bootstrap_script_url by default, got %q", cfg.Bootstrap.OTOBootstrapScriptURL)
}
if cfg.Bootstrap.OTOReleaseBaseURL != "" {
t.Errorf("expected empty bootstrap.oto_release_base_url by default, got %q", cfg.Bootstrap.OTOReleaseBaseURL)
}
}
func TestLoadEdge_OTOBootstrapExplicit(t *testing.T) {
dir := t.TempDir()
f := filepath.Join(dir, "edge.yaml")
yaml := `server:
listen: "0.0.0.0:9090"
bootstrap:
oto_bootstrap_script_url: "https://example.com/bootstrap.sh"
oto_release_base_url: "https://example.com/releases"
`
if err := os.WriteFile(f, []byte(yaml), 0o600); err != nil {
t.Fatalf("write yaml: %v", err)
}
cfg, err := config.LoadEdge(f)
if err != nil {
t.Fatalf("load: %v", err)
}
if cfg.Bootstrap.OTOBootstrapScriptURL != "https://example.com/bootstrap.sh" {
t.Errorf("bootstrap.oto_bootstrap_script_url=%q", cfg.Bootstrap.OTOBootstrapScriptURL)
}
if cfg.Bootstrap.OTOReleaseBaseURL != "https://example.com/releases" {
t.Errorf("bootstrap.oto_release_base_url=%q", cfg.Bootstrap.OTOReleaseBaseURL)
}
}
func TestLoadEdge_A2ADefaults(t *testing.T) {
dir := t.TempDir()
f := filepath.Join(dir, "edge.yaml")