diff --git a/agent-roadmap/current.md b/agent-roadmap/current.md index e5e6459..a9cbd9d 100644 --- a/agent-roadmap/current.md +++ b/agent-roadmap/current.md @@ -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` ## 선택 규칙 diff --git a/agent-roadmap/phase/automation-runtime-bridge/PHASE.md b/agent-roadmap/phase/automation-runtime-bridge/PHASE.md index 08f53ee..13f722a 100644 --- a/agent-roadmap/phase/automation-runtime-bridge/PHASE.md +++ b/agent-roadmap/phase/automation-runtime-bridge/PHASE.md @@ -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 표면을 구현한다. diff --git a/agent-roadmap/phase/automation-runtime-bridge/milestones/oto-bootstrap-command-issue.md b/agent-roadmap/phase/automation-runtime-bridge/milestones/oto-bootstrap-command-issue.md index 4eee5ee..73fe1ab 100644 --- a/agent-roadmap/phase/automation-runtime-bridge/milestones/oto-bootstrap-command-issue.md +++ b/agent-roadmap/phase/automation-runtime-bridge/milestones/oto-bootstrap-command-issue.md @@ -12,7 +12,7 @@ Edge는 OTO bootstrap script를 구현하지 않고, OTO가 소유한 script 인 ## 상태 -[계획] +[진행중] ## 구현 잠금 diff --git a/agent-task/archive/2026/06/m-oto-bootstrap-command-issue/01_oto_bootstrap_config/code_review_local_G05_0.log b/agent-task/archive/2026/06/m-oto-bootstrap-command-issue/01_oto_bootstrap_config/code_review_local_G05_0.log new file mode 100644 index 0000000..38ab4ac --- /dev/null +++ b/agent-task/archive/2026/06/m-oto-bootstrap-command-issue/01_oto_bootstrap_config/code_review_local_G05_0.log @@ -0,0 +1,188 @@ + + +# 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-`이면 완료 이벤트 메타데이터를 보고한다. 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회 실패했으나, 해당 단일 테스트와 전체 범위 재실행이 이어서 통과했다. diff --git a/agent-task/archive/2026/06/m-oto-bootstrap-command-issue/01_oto_bootstrap_config/complete.log b/agent-task/archive/2026/06/m-oto-bootstrap-command-issue/01_oto_bootstrap_config/complete.log new file mode 100644 index 0000000..974d9c7 --- /dev/null +++ b/agent-task/archive/2026/06/m-oto-bootstrap-command-issue/01_oto_bootstrap_config/complete.log @@ -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 + +- 없음 + +## 후속 작업 + +- 없음 diff --git a/agent-task/archive/2026/06/m-oto-bootstrap-command-issue/01_oto_bootstrap_config/plan_local_G05_0.log b/agent-task/archive/2026/06/m-oto-bootstrap-command-issue/01_oto_bootstrap_config/plan_local_G05_0.log new file mode 100644 index 0000000..86a28a1 --- /dev/null +++ b/agent-task/archive/2026/06/m-oto-bootstrap-command-issue/01_oto_bootstrap_config/plan_local_G05_0.log @@ -0,0 +1,181 @@ + + +# 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`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/agent-task/m-oto-bootstrap-command-issue/02+01_agent_register_command/CODE_REVIEW-cloud-G07.md b/agent-task/m-oto-bootstrap-command-issue/02+01_agent_register_command/CODE_REVIEW-cloud-G07.md new file mode 100644 index 0000000..5e4e77d --- /dev/null +++ b/agent-task/m-oto-bootstrap-command-issue/02+01_agent_register_command/CODE_REVIEW-cloud-G07.md @@ -0,0 +1,160 @@ + + +# 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-`이면 완료 이벤트 메타데이터를 보고한다. 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. diff --git a/agent-task/m-oto-bootstrap-command-issue/02+01_agent_register_command/PLAN-cloud-G07.md b/agent-task/m-oto-bootstrap-command-issue/02+01_agent_register_command/PLAN-cloud-G07.md new file mode 100644 index 0000000..7f6828f --- /dev/null +++ b/agent-task/m-oto-bootstrap-command-issue/02+01_agent_register_command/PLAN-cloud-G07.md @@ -0,0 +1,267 @@ + + +# 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 --bootstrap-script-url --release-base-url +``` + +Flags: + +- `--alias` default `` +- `--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 | bash -s -- --edge-url --agent-id --enrollment-token --release-base-url --agent-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`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다. diff --git a/apps/edge/cmd/edge/main.go b/apps/edge/cmd/edge/main.go index 7be0a7c..fd4d86a 100644 --- a/apps/edge/cmd/edge/main.go +++ b/apps/edge/cmd/edge/main.go @@ -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", diff --git a/apps/edge/cmd/edge/main_test.go b/apps/edge/cmd/edge/main_test.go index ed82707..04f9977 100644 --- a/apps/edge/cmd/edge/main_test.go +++ b/apps/edge/cmd/edge/main_test.go @@ -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) + } +} diff --git a/configs/edge.yaml b/configs/edge.yaml index bfc8b14..56488b2 100644 --- a/configs/edge.yaml +++ b/configs/edge.yaml @@ -13,6 +13,8 @@ server: # field channel is provisioned. bootstrap: artifact_base_url: "" + oto_bootstrap_script_url: "" + oto_release_base_url: "" tls: enabled: false diff --git a/packages/go/config/config.go b/packages/go/config/config.go index 2aecab8..2ca5651 100644 --- a/packages/go/config/config.go +++ b/packages/go/config/config.go @@ -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") diff --git a/packages/go/config/config_test.go b/packages/go/config/config_test.go index e08964c..b106c0d 100644 --- a/packages/go/config/config_test.go +++ b/packages/go/config/config_test.go @@ -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")