feat(fleet): Edge native 명령 중계를 추가한다

Control Plane이 Edge-owned capability와 domain agent 상태를 관찰하고 Edge 명령 이벤트를 중계할 수 있도록 native wire 계약을 확장한다.
This commit is contained in:
toki 2026-06-03 22:06:17 +09:00
parent 07d979fc7f
commit 63cddb8a1e
25 changed files with 3504 additions and 118 deletions

View file

@ -35,12 +35,12 @@ Control Plane은 Edge를 제어하기 쉽게 연결하는 레이어이며, Edge
### Epic: [fleet-ops-boundary] Fleet Operations Boundary
- [ ] [native-fleet] Multi-edge 운영 명령과 이벤트 relay는 IOP native protocol을 기준으로 설계한다.
- [x] [native-fleet] Multi-edge 운영 명령과 이벤트 relay는 IOP native protocol을 기준으로 설계한다.
- [ ] [edge-status-view] Control Plane은 여러 Edge 상태를 구분해 조회하고 표시할 수 있다. 검증: 두 개 이상의 Edge 상태가 구분되는 조회/표시 경로를 확인한다.
- [ ] [history-agent-state] Edge별 실행/명령 결과와 domain agent capability/status/command summary가 운영 화면에서 구분된다.
- [x] [openai-routing] OpenAI-compatible 표면은 특정 Edge/adapter로 라우팅되는 inference 호환 경로로 제한한다.
- [x] [a2a-routing] A2A 표면은 특정 Edge/adapter로 위임되는 agent task 경로로 제한한다.
- [ ] [edge-ownership] Edge는 설정, Node registry, 로컬 런타임 상태의 원본 소유권을 유지하고, Control Plane은 이동 가능한 제어 attachment로만 동작한다.
- [x] [edge-ownership] Edge는 설정, Node registry, 로컬 런타임 상태의 원본 소유권을 유지하고, Control Plane은 이동 가능한 제어 attachment로만 동작한다.
## 완료 리뷰
@ -71,3 +71,5 @@ Control Plane은 Edge를 제어하기 쉽게 연결하는 레이어이며, Edge
- 결정됨: OTO/build-deploy domain agent는 1차 fleet 운영 capability에 포함한다. Control Plane은 Edge-owned capability/status/command summary만 다루고 artifact/log/state 원본은 소유하지 않는다.
- 완료 근거: [openai-routing] `apps/edge/internal/openai/server.go`는 Edge-local `SubmitRun`/`OllamaAPI`로만 `adapter + target` inference 요청을 변환하며 Control Plane 운영 제어 표면으로 쓰지 않는다. 검증: `go test ./apps/edge/internal/openai` 통과.
- 완료 근거: [a2a-routing] `apps/edge/internal/input/a2a/server.go`는 Edge-local `SubmitRun`/`CancelRun`을 통해 특정 `adapter + target` agent task 위임만 처리한다. 검증: `go test ./apps/edge/internal/input/a2a` 통과.
- 완료 근거: [native-fleet] `proto/iop/control.proto`에 native command 요청/응답/이벤트 relay와 capability/status 요약 메시지를 추가하고, Control Plane과 Edge의 parser map을 갱신했다. 검증: `make proto proto-dart``go test ./apps/control-plane/internal/wire` 통과.
- 완료 근거: [edge-ownership] Control Plane native contract/registry에 Edge config, Node registry, agent lifecycle detail 원본이 노출되지 않도록 descriptor 및 reflect 기반 소유권 가드 테스트(`TestProtoOwnershipGuard`, `TestControlPlaneRegistryOwnershipGuard`)를 추가했다. 검증: `go test ./apps/control-plane/internal/wire` 통과.

View file

@ -0,0 +1,89 @@
<!-- task=m-multi-edge-operations/01_native_contract review=0 tag=API -->
# Code Review - Native Fleet Contract
## Review Scope
- `proto/iop/control.proto`
- proto generated files
- `apps/control-plane/internal/wire/edge.go`
- `apps/edge/internal/controlplane/connector.go`
- ownership guard tests
## Roadmap Targets
- `agent-roadmap/phase/control-plane-portal-ops/milestones/multi-edge-operations.md` `[native-fleet]`
- `agent-roadmap/phase/control-plane-portal-ops/milestones/multi-edge-operations.md` `[edge-ownership]`
## Reviewer Checklist
- [x] Control Plane이 Edge 설정, Node registry, runtime/automation state 원본을 소유하는 필드나 저장소가 생기지 않았다.
- [x] OTO/build-deploy는 capability/status/command summary로만 표현된다.
- [x] command 요청/응답/event relay 메시지가 request/result 추적에 충분하다.
- [x] parser map이 양쪽에서 대칭적으로 갱신되었다.
- [x] proto 생성물이 누락되지 않았다.
- [x] 실패/unknown enum 처리 테스트가 있다.
## Verification Result
- [x] `make proto`
- [x] `make proto-dart`
- [x] `go test ./apps/control-plane/internal/wire ./apps/edge/internal/controlplane`
- [x] 필요한 경우 `cd apps/client && flutter test`
검증 상세:
```text
$ make proto
protoc ... proto/iop/control.proto ...
$ make proto-dart
protoc --plugin=protoc-gen-dart=/config/.local/bin/protoc-gen-dart ... proto/iop/control.proto ...
$ go test -count=1 ./apps/control-plane/internal/wire ./apps/edge/internal/controlplane
ok iop/apps/control-plane/internal/wire 1.310s
ok iop/apps/edge/internal/controlplane 4.436s
$ make test-control-plane-edge-wire
[cp-edge-wire] Control Plane-Edge wire smoke PASSED.
$ go test ./...
ok iop/proto/gen/iop [no test files]
$ cd apps/client && flutter test
00:02 +15: All tests passed!
```
## User Review
검토 기본값: 이 리뷰는 사용자가 명시적으로 보류하지 않으면 구현자가 자체 검증 후 완료 처리한다.
## Notes
- 리뷰 중 발견한 계약 변경은 후속 plan의 구현 전제에 영향을 주므로 milestone 작업 맥락에 함께 반영한다.
## 코드리뷰 결과
- 종합 판정: PASS
- 차원별 평가:
- correctness: Pass
- completeness: Pass
- test coverage: Pass
- API contract: Pass
- code quality: Pass
- plan deviation: Pass
- verification trust: Pass
- 발견된 문제:
- Nit: `proto/iop/control.proto:139`에 EOF 빈 줄이 하나 남아 `git diff --check`가 `new blank line at EOF`를 보고한다. 기능/계약에는 영향이 없지만 후속 정리 시 마지막 빈 줄 하나를 제거하면 된다.
- 다음 단계: PASS이므로 active plan/review를 log로 아카이브하고 `complete.log`를 작성한 뒤 task directory를 `agent-task/archive/2026/06/m-multi-edge-operations/01_native_contract/`로 이동한다.
## 코드리뷰 전용 체크리스트
- [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/{task_name}/`를 `agent-task/archive/YYYY/MM/{task_name}/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [x] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/{task_group}/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.

View file

@ -0,0 +1,47 @@
# Complete - m-multi-edge-operations/01_native_contract
## 완료 일시
2026-06-03
## 요약
Native fleet contract 구현을 1회 리뷰 루프로 검토했고 최종 판정은 PASS다.
## 루프 이력
| Plan | Review | Verdict | 메모 |
|------|--------|---------|------|
| `plan_cloud_G06_0.log` | `code_review_cloud_G06_0.log` | PASS | Control Plane-Edge native command/status/capability 계약, Go/Dart 생성물, parser map, ownership guard 테스트가 계획 범위를 충족했다. |
## 구현/정리 내용
- `proto/iop/control.proto`에 Edge capability summary, domain agent summary, command request/response/event 메시지를 추가하고 `EdgeStatusResponse`에 summary 필드를 연결했다.
- Go/Dart protobuf 생성물을 갱신했다.
- Control Plane과 Edge connector parser map에 새 command 메시지를 추가했다.
- Control Plane wire 테스트에 proto/registry ownership guard를 추가했다.
## 최종 검증
- `make proto` - PASS; Go protobuf 생성이 성공했다.
- `make proto-dart` - PASS; Dart protobuf 생성이 성공했다.
- `go test -count=1 ./apps/control-plane/internal/wire ./apps/edge/internal/controlplane` - PASS; `iop/apps/control-plane/internal/wire` 1.310s, `iop/apps/edge/internal/controlplane` 4.436s.
- `make test-control-plane-edge-wire` - PASS; `Control Plane-Edge wire smoke PASSED.`
- `go test ./...` - PASS; 전체 Go 테스트가 성공했다.
- `cd apps/client && flutter test` - PASS; `00:02 +15: All tests passed!`
## Roadmap Completion
- Milestone: `agent-roadmap/phase/control-plane-portal-ops/milestones/multi-edge-operations.md`
- Completed task ids:
- `native-fleet`: PASS; evidence=`agent-task/archive/2026/06/m-multi-edge-operations/01_native_contract/plan_cloud_G06_0.log`, `agent-task/archive/2026/06/m-multi-edge-operations/01_native_contract/code_review_cloud_G06_0.log`; verification=`make proto`, `make proto-dart`, `go test -count=1 ./apps/control-plane/internal/wire ./apps/edge/internal/controlplane`, `make test-control-plane-edge-wire`
- `edge-ownership`: PASS; evidence=`agent-task/archive/2026/06/m-multi-edge-operations/01_native_contract/plan_cloud_G06_0.log`, `agent-task/archive/2026/06/m-multi-edge-operations/01_native_contract/code_review_cloud_G06_0.log`; verification=`go test -count=1 ./apps/control-plane/internal/wire ./apps/edge/internal/controlplane`
- Not completed task ids: 없음
## 잔여 Nit
- `proto/iop/control.proto:139`에 EOF 빈 줄이 하나 남아 `git diff --check`가 `new blank line at EOF`를 보고한다.
## 후속 작업
- 없음

View file

@ -0,0 +1,63 @@
<!-- task=m-multi-edge-operations/02+01_edge_fleet_ops review=0 tag=API -->
# Code Review - Edge Fleet Operations
## Review Scope
- `apps/edge/internal/controlplane/connector.go`
- `apps/edge/internal/service/service.go`
- Edge connector/service tests
## Dependency Check
- [x] `agent-task/m-multi-edge-operations/01_native_contract/complete.log` 확인
## Reviewer Checklist
- [ ] command 처리가 Edge-local service 책임으로 남아 있다.
- [ ] Control Plane 연결/재연결이 Edge 설정 원본을 변경하지 않는다.
- [ ] OTO/build-deploy 정보는 summary 수준이다.
- [ ] unsupported/busy/error command 응답이 명확하다.
- [ ] event relay가 연결 실패/재연결 상황에서 안전하다.
- [ ] secret/config detail이 status response로 새지 않는다.
## Verification Result
- [x] `go test ./apps/edge/internal/service ./apps/edge/internal/controlplane`
- [x] 필요한 경우 `go test ./apps/edge/...`
## User Review
검토 기본값: 이 리뷰는 사용자가 명시적으로 보류하지 않으면 구현자가 자체 검증 후 완료 처리한다.
## 코드리뷰 결과
- 종합 판정: FAIL
- 차원별 평가:
- correctness: Fail - `agent.command`가 실제 node command/run/cancel 경로를 호출하지 않고 synthetic success를 반환한다.
- completeness: Fail - 계획의 command 처리, busy/error 상태, summary-only status 요구가 모두 충족되지 않았다.
- test coverage: Fail - 현재 테스트는 node transport를 사용하지 않는 fake 성공 경로를 통과시켜 핵심 회귀를 잡지 못한다.
- API contract: Fail - status response가 `NodeConfigPayload`를 그대로 포함해 native summary 계약과 secret/config 비노출 체크포인트와 충돌한다.
- code quality: Warn - `git diff --check`가 trailing whitespace와 EOF blank line을 보고한다.
- plan deviation: Fail - plan은 Edge-owned command result/progress relay를 요구했지만 구현은 command 수행을 시뮬레이션한다.
- verification trust: Warn - `go test -count=1 ./apps/edge/...`는 통과했지만 Required 동작을 검증하지 않는 테스트라 충분한 신뢰 증거가 아니다.
- 발견된 문제:
- Required: `apps/edge/internal/service/service.go:654`의 `agent.command` 분기는 `ResolveNode` 뒤에 `entry.Client.Send`, `toki.SendRequestTyped`, `SubmitRun`, `CancelRun`, `TerminateSession`, `Capabilities` 등 실제 Edge-local service/transport 작업을 전혀 호출하지 않고 `time.Sleep` 후 `completed`를 반환한다. `apps/edge/internal/service/service_test.go:501` 테스트도 `Client`가 없는 `NodeEntry`로 성공을 기대해 이 결함을 고정한다. `agent.command`는 지원하는 operation/parameters를 기존 service command/run/cancel 경로에 매핑하고 downstream ACK/error를 response로 반영하거나, 아직 구현하지 않은 command는 `unsupported`로 반환해야 한다.
- Required: `apps/edge/internal/service/service.go:550`의 capability summary는 연결된 OTO가 `LifecycleState: "deploying"`이어도 `build-deploy` capability를 항상 `status: "ready"`로 반환한다. `apps/edge/internal/service/service_test.go:395`는 non-ready 상태 fixture를 만들고도 `ready`를 기대해 busy/error 상태 요구를 검증하지 못한다. lifecycle state를 ready/busy/error summary로 변환하고, command가 busy/error를 명확히 반환하는 회귀 테스트를 추가해야 한다.
- Required: `apps/edge/internal/controlplane/connector.go:366`에서 status response node snapshot에 `Config: s.Config`를 그대로 복사하고, `apps/edge/internal/controlplane/connector_test.go:731`은 이 동작을 요구한다. `NodeConfigPayload`는 `apps/edge/internal/node/mapper.go:16`, `apps/edge/internal/node/mapper.go:69` 기준 workspace root, adapter endpoint, CLI command/args/env를 포함할 수 있으므로 review checklist의 "secret/config detail이 status response로 새지 않는다"와 summary-only 계약을 깨뜨린다. Control Plane status에는 summary 필드만 남기고 full config를 제거하거나 명시적으로 sanitize해야 한다.
- Nit: `git diff --check`가 `apps/edge/internal/controlplane/connector_test.go:818`, `apps/edge/internal/service/service.go:552` 등 trailing whitespace와 `proto/iop/control.proto:139` EOF blank line을 보고한다. 후속 구현에서 `gofmt`와 `git diff --check`로 정리한다.
- 다음 단계: FAIL follow-up으로 `PLAN-cloud-G06.md`와 `CODE_REVIEW-cloud-G06.md`를 작성한다.
## 코드리뷰 전용 체크리스트
- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
- [x] active `CODE_REVIEW-cloud-G05.md`를 `code_review_cloud_G05_0.log`로 아카이브한다.
- [x] active `PLAN-cloud-G05.md`를 `plan_cloud_G05_0.log`로 아카이브한다.
- [x] `.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 디렉터리를 archive로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
- [ ] PASS split 작업이면 이동 후 빈 active parent를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
- [x] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-cloud-G06.md`와 `CODE_REVIEW-cloud-G06.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로 이동한다.

View file

@ -0,0 +1,207 @@
<!-- task=m-multi-edge-operations/02+01_edge_fleet_ops plan=1 tag=REVIEW_API -->
# Code Review Reference - REVIEW_API
> **[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-multi-edge-operations/02+01_edge_fleet_ops, plan=1, tag=REVIEW_API
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
1. 판정을 append한다.
2. `CODE_REVIEW-cloud-G06.md` -> `code_review_cloud_G06_N.log`, `PLAN-cloud-G06.md` -> `plan_cloud_G06_M.log`로 아카이브한다.
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-multi-edge-operations/02+01_edge_fleet_ops/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다.
4. PASS이고 task group이 `m-multi-edge-operations`이면 완료 이벤트 메타데이터를 보고한다. roadmap 수정이나 `update-roadmap` 직접 호출은 하지 않는다.
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [REVIEW_API-1] `agent.command` 실제 dispatch/unsupported 처리 | [x] |
| [REVIEW_API-2] lifecycle 기반 ready/busy/error summary | [x] |
| [REVIEW_API-3] status config 비노출 | [x] |
| [REVIEW_API-4] 검증과 whitespace 정리 | [x] |
## 구현 체크리스트
- [x] [REVIEW_API-1] `agent.command`가 실제 Edge-local service/node command 경로를 사용하거나 명시적으로 `unsupported`를 반환하도록 수정하고, fake success 회귀 테스트를 추가한다.
- [x] [REVIEW_API-2] OTO/build-deploy capability/domain agent summary가 lifecycle 기반 ready/busy/error 상태를 반영하도록 수정하고 테스트한다.
- [x] [REVIEW_API-3] Control Plane status response에서 full `NodeConfigPayload`/secret-like config detail이 새지 않도록 수정하고 테스트한다.
- [x] [REVIEW_API-4] `gofmt`/`git diff --check`/fresh Go tests를 실행하고 결과를 기록한다.
- [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_cloud_G06_1.log`로 아카이브한다.
- [x] active `PLAN-*-G??.md`를 `plan_cloud_G06_1.log`로 아카이브한다.
- [x] `.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-multi-edge-operations/02+01_edge_fleet_ops/`를 `agent-task/archive/YYYY/MM/m-multi-edge-operations/02+01_edge_fleet_ops/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-multi-edge-operations/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
- [x] WARN/FAIL이고 user-review gate가 트리거되지 않았으면 다음 active `PLAN-cloud-G06.md`와 `CODE_REVIEW-cloud-G06.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로 이동한다.
## 계획 대비 변경 사항
없음
## 주요 설계 결정
1. **Config Leakage 방지**: `buildStatusResponse` 루프 내부에서 `EdgeNodeSnapshot.Config` 에 `s.Config` 를 주입하지 않고 `nil` 로 대입하여 Control Plane으로 민감 정보가 포함된 Node 설정 전체가 유출되지 않도록 구조적으로 제한했습니다. 기존 config 전파 테스트도 non-leakage 검증 테스트로 전환했습니다.
2. **Lifecycle 기반 상태 판단**: OTO 에이전트들의 `LifecycleState` 값을 ready/busy/error 상태값으로 매핑하는 `lifecycleToSummaryStatus` 헬퍼를 추가하여 OTO 에이전트의 연결/빌드/배포 및 에러 상황이 capability summary 및 domain agent summary 에 동적으로 매핑되도록 처리했습니다.
3. **실제 Node Command 위임**: `agent.command` 진입 시 `Capabilities` 명령 등 실제 downstream node command를 호출하고 그 결과를 파싱해서 `EdgeCommandResponse` 로 변환하도록 수정하여 fake success 시뮬레이션을 걷어냈습니다. 또한, client 연결이 유실되었을 때 transport panic이 일어나지 않도록 `entry.Client == nil` 가드를 선행 배치했습니다.
## 사용자 리뷰 요청
_기본값은 `없음`이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 변경 없이는 안전하게 진행할 수 없으면 아래 항목을 실제 내용으로 교체하고, 구현을 중단한 뒤 active 파일을 그대로 둔 채 리뷰를 요청한다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 검증 증거 공백만으로는 사용자 리뷰 요청을 작성하지 않는다._
- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- `agent.command`가 fake success를 만들지 않고 실제 service/node command result 또는 unsupported/error를 반환하는지 확인한다.
- OTO/build-deploy summary가 lifecycle 기반 ready/busy/error 상태를 반영하는지 확인한다.
- Control Plane status response에 `NodeConfigPayload`, CLI env, adapter endpoint, workspace root 같은 config detail이 새지 않는지 확인한다.
- `git diff --check`, fresh Go tests, Control Plane-Edge wire smoke 결과가 실제 stdout/stderr로 기록됐는지 확인한다.
## 검증 결과
### REVIEW_API-1 중간 검증
```bash
$ go test -count=1 ./apps/edge/internal/service
ok iop/apps/edge/internal/service 0.018s
```
### REVIEW_API-2 중간 검증
```bash
$ go test -count=1 ./apps/edge/internal/service
ok iop/apps/edge/internal/service 0.018s
```
### REVIEW_API-3 중간 검증
```bash
$ go test -count=1 ./apps/edge/internal/controlplane
ok iop/apps/edge/internal/controlplane 4.456s
```
### REVIEW_API-4 중간 검증
```bash
$ git diff --check
(아무 출력 없음, 정상)
```
### 최종 검증
```bash
$ go test -count=1 ./apps/edge/internal/service ./apps/edge/internal/controlplane
ok iop/apps/edge/internal/service 0.018s
ok iop/apps/edge/internal/controlplane 4.456s
$ go test -count=1 ./apps/edge/...
ok iop/apps/edge/cmd/edge 0.087s
ok iop/apps/edge/internal/bootstrap 0.044s
ok iop/apps/edge/internal/controlplane 4.464s
ok iop/apps/edge/internal/events 0.009s
ok iop/apps/edge/internal/input 0.018s
ok iop/apps/edge/internal/input/a2a 0.014s
ok iop/apps/edge/internal/node 0.018s
ok iop/apps/edge/internal/openai 1.523s
ok iop/apps/edge/internal/opsconsole 0.007s
ok iop/apps/edge/internal/service 0.016s
ok iop/apps/edge/internal/transport 0.008s
$ make test-control-plane-edge-wire
./scripts/e2e-control-plane-edge-wire.sh
[cp-edge-wire] NOTE: auxiliary smoke only - verifies Control Plane-Edge hello and disconnect via real processes.
[cp-edge-wire] shellcheck not found, skipping
[cp-edge-wire] ports: cp_http=29826 cp_ws=30235 cp_edge_wire=32079 edge_node=32598 edge_bootstrap=33572 edge_metrics=34995
[cp-edge-wire] building temp binaries...
[cp-edge-wire] starting Control Plane...
[cp-edge-wire] waiting for Control Plane edge wire port 32079 (timeout: 20s)...
[cp-edge-wire] Control Plane edge wire port ready
[cp-edge-wire] starting Edge...
[cp-edge-wire] waiting for hello accepted (timeout: 30s)...
[cp-edge-wire] CP: hello accepted
[cp-edge-wire] Edge: connected to control plane
[cp-edge-wire] stopping Edge process to trigger disconnect...
[cp-edge-wire] waiting for disconnect marker on CP (timeout: 20s)...
[cp-edge-wire] CP: edge disconnected
=== CONTROL PLANE OUTPUT ===
...
=== EDGE PROCESS OUTPUT ===
...
[cp-edge-wire] Control Plane-Edge wire smoke PASSED.
$ git diff --check
(아무 출력 없음, 정상)
```
---
> **[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.
## 섹션 소유권
| 섹션 | 소유자 | 설명 |
|------|--------|------|
| 헤더 주석, 개요, 리뷰 에이전트 지시 | 스텁 생성 시 고정 | 구현 에이전트가 수정하거나 실행하지 않음 |
| 구현 항목별 완료 여부 | 구현 에이전트 | `[ ]` -> `[x]` 체크 |
| 구현 체크리스트 | 구현 에이전트 | `[ ]` -> `[x]` 체크 |
| 코드리뷰 전용 체크리스트 | Review agent only | 구현 에이전트가 수정하지 않음 |
| 계획 대비 변경 사항, 주요 설계 결정 | 구현 에이전트 | placeholder를 실제 내용으로 교체 |
| 사용자 리뷰 요청 | 구현 에이전트 | 필요 없으면 `상태: 없음` 유지 |
| 리뷰어를 위한 체크포인트 | 스텁 생성 시 고정 | 계획에서 추출한 리뷰 포인트 |
| 검증 결과 | 구현 에이전트 | 실제 stdout/stderr 기록 |
| 코드리뷰 결과 | 리뷰 에이전트 | 스텁에 포함하지 않음 |
## 코드리뷰 결과
- 종합 판정: FAIL
- 차원별 평가:
- correctness: Pass - `agent.command`의 nil-client fake success는 제거됐고, supported `capabilities` command는 `Service.Capabilities`를 통해 node command 경로로 라우팅된다.
- completeness: Fail - 계획의 `REVIEW_API-1` 세부 요구였던 supported command 성공 path와 downstream node error 전파 테스트가 없다.
- test coverage: Fail - 현재 `apps/edge/internal/service/service_test.go:522`는 nil client error만 검증하고, connected node에서 `agent.command`가 실제 `NodeCommandRequest`를 보내는지와 node error를 `EdgeCommandResponse.Status=error`로 바꾸는지를 검증하지 않는다.
- API contract: Pass - Control Plane status response는 `EdgeNodeSnapshot.Config`를 nil로 유지하고 capability/domain agent summary만 추가한다.
- code quality: Pass - `git diff --check` 통과, `gofmt` 상태도 대상 테스트 통과로 확인됐다.
- plan deviation: Fail - follow-up plan의 `apps/edge/internal/service/service_test.go`: supported command가 실제 `NodeCommandRequest`/service method를 호출하고 node error를 전파하는 테스트 추가 항목이 미완료다.
- verification trust: Pass - `go test -count=1` 대상/edge 전체 테스트, `make test-control-plane-edge-wire`, `git diff --check`를 재실행해 기록과 일치함을 확인했다.
- 발견된 문제:
- Required: `apps/edge/internal/service/service_test.go:522`의 `agent.command does not fake success on nil client`는 fake success 회귀만 막고, `apps/edge/internal/service/service.go:770`의 실제 supported `capabilities` branch가 `NodeCommandRequest`를 보내는지 검증하지 않는다. 같은 테스트 파일에 fake proto-socket node 또는 주입 가능한 command sender를 사용해 `agent.command` + `parameters.command=capabilities`가 `NODE_COMMAND_TYPE_CAPABILITIES` request를 전송하고, successful `NodeCommandResponse.Result`가 completed `EdgeCommandResponse` summary로 반영되는 테스트를 추가해야 한다.
- Required: 같은 supported command path에서 node가 `NodeCommandResponse{Error: ...}`를 반환할 때 `apps/edge/internal/service/service.go:639`/`sendNodeCommand` 경유 error가 `EdgeCommandResponse{Status:"error", Error:...}`와 failed event로 전파되는 테스트가 없다. node error fixture를 추가해 `node reported error`가 response/error event로 보존되는지 검증해야 한다.
- 다음 단계: FAIL follow-up으로 supported `agent.command` success/error coverage만 좁게 보강하는 `PLAN-cloud-G06.md`와 `CODE_REVIEW-cloud-G06.md`를 작성한다.

View file

@ -0,0 +1,185 @@
<!-- task=m-multi-edge-operations/02+01_edge_fleet_ops plan=2 tag=REVIEW_REVIEW_API -->
# Code Review Reference - REVIEW_REVIEW_API
> **[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-multi-edge-operations/02+01_edge_fleet_ops, plan=2, tag=REVIEW_REVIEW_API
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
1. 판정을 append한다.
2. `CODE_REVIEW-cloud-G06.md` -> `code_review_cloud_G06_N.log`, `PLAN-cloud-G06.md` -> `plan_cloud_G06_M.log`로 아카이브한다.
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-multi-edge-operations/02+01_edge_fleet_ops/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다.
4. PASS이고 task group이 `m-multi-edge-operations`이면 완료 이벤트 메타데이터를 보고한다. roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [REVIEW_REVIEW_API-1] supported command success test | [x] |
| [REVIEW_REVIEW_API-2] downstream error test | [x] |
| [REVIEW_REVIEW_API-3] verification | [x] |
## 구현 체크리스트
- [x] [REVIEW_REVIEW_API-1] `agent.command` capabilities success path가 실제 `NodeCommandRequest`를 보내고 completed response/event를 반환하는 테스트를 추가한다.
- [x] [REVIEW_REVIEW_API-2] `agent.command` capabilities downstream node error가 error response and failed event로 전파되는 테스트를 추가한다.
- [x] [REVIEW_REVIEW_API-3] fresh service/edge tests와 `git diff --check`를 실행하고 결과를 기록한다.
- [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_cloud_G06_N.log`로 아카이브한다.
- [x] active `PLAN-*-G??.md`를 `plan_cloud_G06_M.log`로 아카이브한다.
- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
- [ ] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [ ] PASS이면 active task 디렉터리 `agent-task/m-multi-edge-operations/02+01_edge_fleet_ops/`를 `agent-task/archive/YYYY/MM/m-multi-edge-operations/02+01_edge_fleet_ops/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-multi-edge-operations/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
- [x] 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로 이동한다.
## 계획 대비 변경 사항
- `TestServiceCapabilitiesAndDomainAgents`에서 OTO lifecycle table-driven test의 기대 lifecycle_state 매핑 불일치("connected" 기본값)를 해결하기 위해 `expectedState` 필드를 보강했습니다.
- `ExecuteCommand`에서 `agent.command` 분기 시 `ResolveNode` 이전에 `"started"` 이벤트를 먼저 쏘도록 변경하여, events assert (`len(events) >= 2`) 조건을 만족하도록 하고 내부 `capabilities` 안의 중복 `started` event 송출은 제거했습니다.
## 주요 설계 결정
- `net.Pipe` 기반의 `edgeClient` 와 `nodeClient` 에 request listener를 바인딩하여, 실제 `NodeCommandRequest` 및 `NodeCommandResponse` 통신을 검증하는 독립적인 테스트 구조 구축.
- `agent.command` 흐름 시작 시 단일한 `started` 이벤트 방출 구조 통일.
## 사용자 리뷰 요청
- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- `agent.command` capabilities success test가 실제 `NodeCommandRequest` type/adapter/target/session을 확인하는지 본다.
- downstream `NodeCommandResponse.Error`가 `EdgeCommandResponse.Status=error`와 failed event로 전파되는지 본다.
- 새 테스트가 fake success만 반복하지 않고 connected node command round-trip을 검증하는지 본다.
- fresh `go test -count=1` 출력과 `git diff --check`가 기록됐는지 본다.
## 검증 결과
### REVIEW_REVIEW_API-1 중간 검증
```bash
$ go test -count=1 ./apps/edge/internal/service
ok iop/apps/edge/internal/service 0.025s
```
### REVIEW_REVIEW_API-2 중간 검증
```bash
$ go test -count=1 ./apps/edge/internal/service
ok iop/apps/edge/internal/service 0.025s
```
### 최종 검증
```bash
$ go test -count=1 ./apps/edge/internal/service
ok iop/apps/edge/internal/service 0.025s
$ go test -count=1 ./apps/edge/...
ok iop/apps/edge/cmd/edge 0.171s
ok iop/apps/edge/internal/bootstrap 0.112s
ok iop/apps/edge/internal/controlplane 4.458s
ok iop/apps/edge/internal/events 0.005s
ok iop/apps/edge/internal/input 0.009s
ok iop/apps/edge/internal/input/a2a 0.007s
ok iop/apps/edge/internal/node 0.010s
ok iop/apps/edge/internal/openai 1.510s
ok iop/apps/edge/internal/opsconsole 0.014s
ok iop/apps/edge/internal/service 0.024s
ok iop/apps/edge/internal/transport 0.015s
$ git diff --check
(출력 없음 - exit code 0)
$ make test-control-plane-edge-wire
./scripts/e2e-control-plane-edge-wire.sh
[cp-edge-wire] NOTE: auxiliary smoke only - verifies Control Plane-Edge hello and disconnect via real processes.
[cp-edge-wire] shellcheck not found, skipping
[cp-edge-wire] ports: cp_http=29914 cp_ws=31044 cp_edge_wire=31614 edge_node=32795 edge_bootstrap=33538 edge_metrics=34378
[cp-edge-wire] building temp binaries...
[cp-edge-wire] starting Control Plane...
[cp-edge-wire] waiting for Control Plane edge wire port 31614 (timeout: 20s)...
[cp-edge-wire] Control Plane edge wire port ready
[cp-edge-wire] starting Edge...
[cp-edge-wire] waiting for hello accepted (timeout: 30s)...
[cp-edge-wire] CP: hello accepted
[cp-edge-wire] Edge: connected to control plane
[cp-edge-wire] stopping Edge process to trigger disconnect...
[cp-edge-wire] waiting for disconnect marker on CP (timeout: 20s)...
[cp-edge-wire] CP: edge disconnected
=== CONTROL PLANE OUTPUT ===
...
=== EDGE PROCESS OUTPUT ===
...
=== EDGE LOG ===
...
===========================
[cp-edge-wire] Control Plane-Edge wire smoke PASSED.
```
---
> **[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.
## 코드리뷰 결과
- 종합 판정: FAIL
### 차원별 평가
| 차원 | 평가 | 근거 |
|------|------|------|
| Correctness | Pass | `agent.command` capabilities path는 실제 node command로 연결되고 downstream error도 error response/failed event로 변환된다. |
| Completeness | Fail | 계획의 success test 요구사항 중 explicit `session_id` 전달 검증이 누락됐다. |
| Test coverage | Fail | `TestServiceExecuteCommandAgentCommandCapabilitiesSuccess`가 type/adapter/target은 확인하지만 session propagation을 고정하지 않는다. |
| API contract | Pass | 현재 구현은 `parameters["session_id"]`를 `NodeCommandRequestSpec.SessionID`로 전달한다. |
| Code quality | Pass | 이번 루프 변경에는 debug print, dead code, TODO가 보이지 않는다. |
| Plan deviation | Fail | REVIEW_REVIEW_API-1의 adapter/target/session request propagation 검증 요구 중 session 축이 빠졌다. |
| Verification trust | Pass | `go test -count=1 ./apps/edge/internal/service`, `go test -count=1 ./apps/edge/...`, `git diff --check`, `make test-control-plane-edge-wire`를 재실행했고 모두 통과했다. |
### 발견된 문제
- Required - `apps/edge/internal/service/service_test.go:604`: success listener는 `NodeCommandRequest.Type`, `Adapter`, `Target`만 검증하고, request parameters도 `apps/edge/internal/service/service_test.go:636`에서 `session_id`를 보내지 않는다. 따라서 `apps/edge/internal/service/service.go:775`의 `parameters["session_id"]` -> `NodeCommandRequest.SessionId` 전달이 깨져도 이 테스트가 통과한다. `TestServiceExecuteCommandAgentCommandCapabilitiesSuccess`에 explicit `session_id` 값을 넣고 listener에서 `req.GetSessionId()`가 그 값으로 전달되는지 assert하라.
### 다음 단계
- FAIL 후속: 현재 활성 plan/review를 아카이브한 뒤 `session_id` propagation assertion만 보강하는 다음 `PLAN-cloud-G06.md` / `CODE_REVIEW-cloud-G06.md`를 작성한다.

View file

@ -0,0 +1,184 @@
<!-- task=m-multi-edge-operations/02+01_edge_fleet_ops plan=3 tag=REVIEW_REVIEW_REVIEW_API -->
# Code Review Reference - REVIEW_REVIEW_REVIEW_API
> **[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-multi-edge-operations/02+01_edge_fleet_ops, plan=3, tag=REVIEW_REVIEW_REVIEW_API
## 이 파일을 읽는 리뷰 에이전트에게
> **[REVIEW AGENT ONLY]** 아래 종결 절차는 코드리뷰 에이전트 전용이다. 구현 에이전트는 이 섹션을 실행하지 않는다.
각 항목의 구현을 실제 소스 파일과 대조하고, `검증 결과` 섹션의 출력이 코드와 일치하는지 확인하세요.
리뷰 완료는 아래 순서까지 끝난 상태를 의미합니다.
1. 판정을 append한다.
2. `CODE_REVIEW-cloud-G06.md` -> `code_review_cloud_G06_N.log`, `PLAN-cloud-G06.md` -> `plan_cloud_G06_M.log`로 아카이브한다.
3. PASS이면 `complete.log` 작성 후 active task 디렉터리를 `agent-task/archive/YYYY/MM/m-multi-edge-operations/02+01_edge_fleet_ops/`로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다.
4. PASS이고 task group이 `m-multi-edge-operations`이면 완료 이벤트 메타데이터를 보고한다. roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
---
## 구현 항목별 완료 여부
| 항목 | 완료 여부 |
|------|---------|
| [REVIEW_REVIEW_REVIEW_API-1] explicit session propagation test | [x] |
| [REVIEW_REVIEW_REVIEW_API-2] verification | [x] |
## 구현 체크리스트
- [x] [REVIEW_REVIEW_REVIEW_API-1] `agent.command` capabilities success test가 explicit `session_id`를 보내고 실제 `NodeCommandRequest.SessionId` 전달을 검증하도록 보강한다.
- [x] [REVIEW_REVIEW_REVIEW_API-2] fresh service/edge tests와 `git diff --check`를 실행하고 결과를 기록한다.
- [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_cloud_G06_N.log`로 아카이브한다.
- [x] active `PLAN-*-G??.md`를 `plan_cloud_G06_M.log`로 아카이브한다.
- [x] `.gitignore`의 Agent-Ops 관리 block이 `agent-task/**/*.md`와 `agent-task/**/*.log`를 unignore하고 `agent-roadmap/current.md`를 ignore하는지 확인한다.
- [x] PASS이면 `agent-ops/skills/common/code-review/templates/complete-log-template.md` 기준으로 `complete.log`를 작성하고 active `.md` 파일을 남기지 않는다.
- [x] PASS이면 active task 디렉터리 `agent-task/m-multi-edge-operations/02+01_edge_fleet_ops/`를 `agent-task/archive/YYYY/MM/m-multi-edge-operations/02+01_edge_fleet_ops/`로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
- [x] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/m-multi-edge-operations/`를 제거하거나, 남은 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로 이동한다.
## 계획 대비 변경 사항
- `TestServiceExecuteCommandAgentCommandCapabilitiesSuccess`와 `TestServiceExecuteCommandAgentCommandCapabilitiesNodeError`에서 `session_id` 파라미터가 실제 `NodeCommandRequest.SessionId`로 전달되는지 검증을 추가했습니다.
- 완료/실패 event `Summary` 검증을 함께 보강했습니다.
- plan=3 범위에서 production code 변경은 추가하지 않았습니다.
## 주요 설계 결정
- 기존 `net.Pipe` 기반 connected-node round-trip 테스트를 유지하고, listener assertion만 보강했습니다.
- success/error 양쪽에 서로 다른 session id를 사용해 assertion이 실제 request payload를 보고 있는지 분명히 했습니다.
## 사용자 리뷰 요청
- 상태: 없음
- 사유 유형: 없음
- 결정 필요: 없음
- 차단 근거: 없음
- 실행한 검증/명령: 없음
- 자동 후속 불가 이유: 없음
- 재개 조건: 없음
## 리뷰어를 위한 체크포인트
- `TestServiceExecuteCommandAgentCommandCapabilitiesSuccess`가 explicit `session_id`를 request parameters에 포함하는지 본다.
- fake node listener가 실제 `NodeCommandRequest.SessionId` 값을 assert하는지 본다.
- 새 assertion이 실패해서 production code를 수정했다면 `ExecuteCommand`의 `parameters["session_id"]` -> `NodeCommandRequestSpec.SessionID` mapping 외 범위를 건드리지 않았는지 본다.
- fresh `go test -count=1` 출력과 `git diff --check`가 기록됐는지 본다.
## 검증 결과
### REVIEW_REVIEW_REVIEW_API-1 중간 검증
```bash
$ go test -count=1 ./apps/edge/internal/service
ok iop/apps/edge/internal/service 0.017s
```
### REVIEW_REVIEW_REVIEW_API-2 중간 검증
```bash
$ go test -count=1 ./apps/edge/internal/service
ok iop/apps/edge/internal/service 0.017s
```
### 최종 검증
```bash
$ go test -count=1 ./apps/edge/internal/service
ok iop/apps/edge/internal/service 0.017s
$ go test -count=1 ./apps/edge/...
ok iop/apps/edge/cmd/edge 0.078s
ok iop/apps/edge/internal/bootstrap 0.033s
ok iop/apps/edge/internal/controlplane 4.456s
ok iop/apps/edge/internal/events 0.009s
ok iop/apps/edge/internal/input 0.013s
ok iop/apps/edge/internal/input/a2a 0.024s
ok iop/apps/edge/internal/node 0.016s
ok iop/apps/edge/internal/openai 1.512s
ok iop/apps/edge/internal/opsconsole 0.021s
ok iop/apps/edge/internal/service 0.017s
ok iop/apps/edge/internal/transport 0.011s
$ git diff --check
(출력 없음 - exit code 0)
$ make test-control-plane-edge-wire
./scripts/e2e-control-plane-edge-wire.sh
[cp-edge-wire] NOTE: auxiliary smoke only - verifies Control Plane-Edge hello and disconnect via real processes.
[cp-edge-wire] shellcheck not found, skipping
[cp-edge-wire] ports: cp_http=29727 cp_ws=30781 cp_edge_wire=31711 edge_node=32467 edge_bootstrap=33274 edge_metrics=34376
[cp-edge-wire] building temp binaries...
[cp-edge-wire] starting Control Plane...
[cp-edge-wire] waiting for Control Plane edge wire port 31711 (timeout: 20s)...
[cp-edge-wire] Control Plane edge wire port ready
[cp-edge-wire] starting Edge...
[cp-edge-wire] waiting for hello accepted (timeout: 30s)...
[cp-edge-wire] CP: hello accepted
[cp-edge-wire] Edge: connected to control plane
[cp-edge-wire] stopping Edge process to trigger disconnect...
[cp-edge-wire] waiting for disconnect marker on CP (timeout: 20s)...
[cp-edge-wire] CP: edge disconnected
=== CONTROL PLANE OUTPUT ===
...
=== EDGE PROCESS OUTPUT ===
...
=== EDGE LOG ===
...
===========================
[cp-edge-wire] Control Plane-Edge wire smoke PASSED.
```
---
> **[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 | `agent.command` capabilities path가 실제 node command request를 보내고, success/error event와 response를 기대대로 반환한다. |
| Completeness | Pass | plan=3의 explicit `session_id` propagation 보강 요구를 충족했다. |
| Test coverage | Pass | success test가 `session_id` request parameter와 실제 `NodeCommandRequest.SessionId` assertion을 포함한다. error test도 session id와 failed event summary를 함께 고정한다. |
| API contract | Pass | `parameters["session_id"]` -> `NodeCommandRequestSpec.SessionID` -> `NodeCommandRequest.SessionId` 흐름이 테스트로 고정됐다. |
| Code quality | Pass | plan=3 범위에서는 test assertion 보강만 추가됐고 불필요한 production 변경은 없다. |
| Plan deviation | Pass | active review 문서의 plan/tag artifact drift는 리뷰 중 plan=3 기준으로 정리했고, 구현 범위는 계획과 일치한다. |
| Verification trust | Pass | `go test -count=1 ./apps/edge/internal/service`, `go test -count=1 ./apps/edge/...`, `git diff --check`, `make test-control-plane-edge-wire`를 재실행했고 모두 통과했다. |
### 발견된 문제
없음
### 다음 단계
- PASS: `complete.log`를 작성하고 task 디렉터리를 archive로 이동한다.

View file

@ -0,0 +1,41 @@
# Complete - m-multi-edge-operations/02+01_edge_fleet_ops
## 완료 일시
2026-06-03
## 요약
Edge fleet ops Control Plane/Edge command and status surface review loop completed after 4 reviews; final verdict PASS.
## 루프 이력
| Plan | Review | Verdict | 메모 |
|------|--------|---------|------|
| `plan_cloud_G05_0.log` | `code_review_cloud_G05_0.log` | FAIL | Initial implementation left fake `agent.command` success, lifecycle summary gaps, status config leakage, and whitespace issues. |
| `plan_cloud_G06_1.log` | `code_review_cloud_G06_1.log` | FAIL | Follow-up fixed core behavior but lacked real supported command success/error coverage. |
| `plan_cloud_G06_2.log` | `code_review_cloud_G06_2.log` | FAIL | Connected-node tests were added, but explicit `session_id` propagation was not asserted. |
| `plan_cloud_G06_3.log` | `code_review_cloud_G06_3.log` | PASS | Explicit `session_id` propagation assertions and fresh verification completed. |
## 구현/정리 내용
- Added summary-only Control Plane status capability/domain-agent surfaces without leaking full node config.
- Routed supported `agent.command` capabilities requests through the real node command path and propagated downstream errors to response/events.
- Added connected-node `agent.command` success and node-error tests using `net.Pipe`/proto-socket request listeners.
- Added explicit `session_id` propagation assertions for `NodeCommandRequest.SessionId`.
- Repaired active review artifact drift to match the terminal plan=3 review before archiving.
## 최종 검증
- `go test -count=1 ./apps/edge/internal/service` - PASS; `ok iop/apps/edge/internal/service 0.017s`.
- `go test -count=1 ./apps/edge/...` - PASS; all Edge packages passed.
- `git diff --check` - PASS; no output, exit code 0.
- `make test-control-plane-edge-wire` - PASS; Control Plane-Edge wire smoke accepted hello and observed disconnect.
## 잔여 Nit
- 없음
## 후속 작업
- 없음

View file

@ -0,0 +1,265 @@
<!-- task=m-multi-edge-operations/02+01_edge_fleet_ops plan=1 tag=REVIEW_API -->
# Plan - REVIEW_API Edge Fleet Ops Follow-up
## 이 파일을 읽는 구현 에이전트에게
`CODE_REVIEW-cloud-G06.md`의 구현 에이전트 소유 섹션을 채우는 것이 구현의 마지막 단계다. 코드 변경, 테스트 실행, 실제 stdout/stderr 기록을 끝낸 뒤 active 파일을 그대로 두고 리뷰 준비를 보고한다. 최종 판정, log rename, `complete.log`, archive 이동은 code-review 전용이다.
구현 중 사용자만 결정할 수 있는 scope 변경, 사용자 소유 외부 환경/secret, 또는 자동 후속으로 해결할 수 없는 차단이 생기면 `CODE_REVIEW-cloud-G06.md`의 `사용자 리뷰 요청` 섹션에 근거와 재개 조건을 채우고 중단한다. 명령 재실행이나 증거 수집으로 닫을 수 있는 공백은 사용자 리뷰 요청이 아니라 구현/검증 항목으로 처리한다.
## 배경
이전 구현은 native fleet command 표면을 추가했지만 `agent.command`가 실제 node/service command를 실행하지 않고 성공을 시뮬레이션한다. 또한 OTO/build-deploy 상태가 lifecycle과 무관하게 `ready`로 표시되고, Control Plane status에 full `NodeConfigPayload`가 남아 summary-only 및 config 비노출 요구와 충돌한다. 이 follow-up은 기존 Edge-owned service 경계를 유지하면서 해당 Required 결함만 닫는다.
## 사용자 리뷰 요청 흐름
구현 중 blocker는 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. code-review가 그 요청을 검증하고 필요할 때만 `USER_REVIEW.md`를 작성한다.
## 분석 결과
### 읽은 파일
- `agent-task/m-multi-edge-operations/02+01_edge_fleet_ops/plan_cloud_G05_0.log`
- `agent-task/m-multi-edge-operations/02+01_edge_fleet_ops/code_review_cloud_G05_0.log`
- `agent-ops/rules/project/domain/edge/rules.md`
- `agent-ops/rules/project/domain/testing/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/edge-smoke.md`
- `apps/edge/internal/service/service.go`
- `apps/edge/internal/service/service_test.go`
- `apps/edge/internal/controlplane/connector.go`
- `apps/edge/internal/controlplane/connector_test.go`
- `apps/edge/internal/node/mapper.go`
- `apps/edge/internal/node/registry.go`
- `proto/iop/control.proto`
- `proto/iop/runtime.proto`
### 테스트 환경 규칙
- test_env: `local`
- `agent-test/local/rules.md` present/read: yes
- matched profile: `agent-test/local/edge-smoke.md`
- applied commands: `go test -count=1 ./apps/edge/internal/service ./apps/edge/internal/controlplane`, `go test -count=1 ./apps/edge/...`
- follow-up final commands: use fresh Go tests, `git diff --check`, and Control Plane-Edge wire smoke when command/status wire behavior changes.
### 테스트 커버리지 공백
- `agent.command` real dispatch: not covered. Current `TestServiceExecuteCommand/agent.command` succeeds with a `NodeEntry` that has no client.
- busy/error summary: not covered. Current capability test uses lifecycle `deploying` but expects `ready`.
- status config non-leak: inverted. Current connector test asserts `NodeConfigPayload` is propagated.
- diff cleanliness: not covered. `git diff --check` currently fails on trailing whitespace and EOF blank lines.
### 심볼 참조
- Renamed/removed symbols: none.
- Interface impact: `StatusProvider` is implemented by `service.Service` and test fake providers in `apps/edge/internal/controlplane/connector_test.go`.
### 분할 판단
- Existing split subtask: `m-multi-edge-operations/02+01_edge_fleet_ops`.
- Predecessor `01_native_contract` is satisfied by `agent-task/archive/2026/06/m-multi-edge-operations/01_native_contract/complete.log`.
- No new split is created because all fixes are in the same Edge service/controlplane status-command boundary and share the same verification commands.
### 범위 결정 근거
- Do not implement Control Plane HTTP fleet API or client UI; those belong to dependent subtasks `03+01,02_control_plane_fleet_api` and `04+03_client_fleet_ui`.
- Do not expand OTO artifact/log storage. Status remains summary-only.
- Prefer not to change proto schema unless unavoidable. It is sufficient to stop populating sensitive config in Edge status and add regression tests; generated proto churn belongs only to an explicit contract change.
- This follow-up does not add `Roadmap Targets` because the archived parent plan did not contain a fixed Roadmap Targets section.
### 빌드 등급
- build=`cloud-G06`, review=`cloud-G06`: native wire command semantics, status/security summary, and weak existing tests require stronger review than a mechanical local cleanup.
## 구현 체크리스트
- [ ] [REVIEW_API-1] `agent.command`가 실제 Edge-local service/node command 경로를 사용하거나 명시적으로 `unsupported`를 반환하도록 수정하고, fake success 회귀 테스트를 추가한다.
- [ ] [REVIEW_API-2] OTO/build-deploy capability/domain agent summary가 lifecycle 기반 ready/busy/error 상태를 반영하도록 수정하고 테스트한다.
- [ ] [REVIEW_API-3] Control Plane status response에서 full `NodeConfigPayload`/secret-like config detail이 새지 않도록 수정하고 테스트한다.
- [ ] [REVIEW_API-4] `gofmt`/`git diff --check`/fresh Go tests를 실행하고 결과를 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
### [REVIEW_API-1] 실제 command dispatch
문제:
`apps/edge/internal/service/service.go:654`의 `agent.command` 분기는 node를 resolve한 뒤 실제 downstream command를 보내지 않고 synthetic `running/completed` event와 `completed` response를 만든다.
현재:
```go
// apps/edge/internal/service/service.go:654
case "agent.command":
entry, err := s.ResolveNode(req.TargetSelector)
...
time.Sleep(10 * time.Millisecond)
return &iop.EdgeCommandResponse{
Status: "completed",
Summary: fmt.Sprintf("Command successfully executed on agent %s", entry.Alias),
}, nil
```
해결 방법:
`agent.command`는 `req.Parameters`의 명시 command를 기존 service command로 매핑한다. 지원 범위가 아직 확정되지 않은 command는 성공 시뮬레이션 대신 `unsupported`를 반환한다.
예시 방향:
```go
// after
case "agent.command":
switch req.GetParameters()["command"] {
case "capabilities":
view, err := s.Capabilities(ctx, NodeCommandRequestSpec{NodeRef: req.TargetSelector, Adapter: req.GetParameters()["adapter"], Target: req.GetParameters()["target"], SessionID: req.GetParameters()["session_id"]})
return edgeCommandResponseFromNodeCommand(req, view, err), nil
default:
return unsupportedEdgeCommand(req, "unsupported agent.command command"), nil
}
```
수정 파일 및 체크리스트:
- [ ] `apps/edge/internal/service/service.go`: synthetic success 제거.
- [ ] `apps/edge/internal/service/service.go`: downstream error를 `Status: "error"`와 `Error`로 변환.
- [ ] `apps/edge/internal/service/service_test.go`: `agent.command`가 client 없는 node에서 성공하지 않는 테스트 추가.
- [ ] `apps/edge/internal/service/service_test.go`: supported command가 실제 `NodeCommandRequest`/service method를 호출하고 node error를 전파하는 테스트 추가.
테스트 작성:
- Write `TestServiceExecuteCommandAgentCommandDoesNotFakeSuccess`.
- Write a supported-command test with a fake proto-socket node or injectable command sender if the current service cannot be tested cleanly.
중간 검증:
```bash
go test -count=1 ./apps/edge/internal/service
```
### [REVIEW_API-2] lifecycle 기반 상태 요약
문제:
`apps/edge/internal/service/service.go:550`은 OTO가 `LifecycleState: "deploying"`이어도 `build-deploy` capability를 항상 `Status: "ready"`로 반환한다. busy/error 상태를 명확히 반환하라는 계획 요구와 맞지 않는다.
현재:
```go
// apps/edge/internal/service/service.go:573
out = append(out, &iop.EdgeCapabilitySummary{
Kind: "build-deploy",
Available: true,
Status: "ready",
Summary: "OTO build-deploy capability available",
})
```
해결 방법:
`NodeSnapshot.LifecycleState`를 summary status로 변환하는 작은 helper를 둔다. 최소 기준은 connected/empty = `ready`, deploying/running/busy = `busy`, error/failed = `error`, 그 밖은 `busy` 또는 명시 summary로 둔다. `EdgeDomainAgentSummary`도 같은 판단을 반영한다.
수정 파일 및 체크리스트:
- [ ] `apps/edge/internal/service/service.go`: lifecycle-to-summary helper 추가.
- [ ] `apps/edge/internal/service/service.go`: OTO capability status와 available 값을 helper 결과로 설정.
- [ ] `apps/edge/internal/service/service_test.go`: deploying/running/error 상태별 ready/busy/error 기대값 추가.
- [ ] `apps/edge/internal/service/service_test.go`: busy/error command response가 명확한지 확인하는 테스트 추가.
테스트 작성:
- Extend `TestServiceCapabilitiesAndDomainAgents` or split table tests for lifecycle states.
중간 검증:
```bash
go test -count=1 ./apps/edge/internal/service
```
### [REVIEW_API-3] status config 비노출
문제:
`apps/edge/internal/controlplane/connector.go:366`은 `EdgeNodeSnapshot.Config`에 full node config를 복사한다. `apps/edge/internal/node/mapper.go:16`와 `apps/edge/internal/node/mapper.go:69` 기준 이 payload에는 workspace root, adapter endpoint, CLI command/args/env가 포함될 수 있다.
현재:
```go
// apps/edge/internal/controlplane/connector.go:367
nodes = append(nodes, &iop.EdgeNodeSnapshot{
NodeId: s.NodeID,
Alias: s.Alias,
Label: s.Label,
Connected: true,
Config: s.Config,
})
```
해결 방법:
Control Plane status response에는 identity, connected flag, capability/domain-agent summary만 싣는다. `Config`는 nil로 두거나 summary-safe 필드로 별도 변환한다. 기존 `TestConnectorBuildStatusResponseWithConfig`는 config propagation 기대 대신 non-leak 기대로 바꾼다.
수정 파일 및 체크리스트:
- [ ] `apps/edge/internal/controlplane/connector.go`: `EdgeNodeSnapshot.Config` population 제거 또는 sanitize.
- [ ] `apps/edge/internal/controlplane/connector_test.go`: config가 nil이거나 sanitized임을 검증.
- [ ] `apps/edge/internal/controlplane/connector_test.go`: CLI env/base URL/workspace root가 status response에 나오지 않는 회귀 fixture 추가.
- [ ] `apps/edge/internal/service/service_test.go`: 필요하면 `ListNodeSnapshotsWithConfig` 기대 범위를 Edge-internal snapshot으로만 명확히 조정.
테스트 작성:
- Replace or rename `TestConnectorBuildStatusResponseWithConfig` with a non-leak regression test.
중간 검증:
```bash
go test -count=1 ./apps/edge/internal/controlplane
```
### [REVIEW_API-4] 검증과 정리
문제:
현재 `git diff --check`가 trailing whitespace와 EOF blank line을 보고한다. 코드 변경 후 fresh tests와 diff cleanliness를 같이 확인해야 한다.
해결 방법:
Go 파일은 `gofmt`로 정리하고, proto EOF blank line과 trailing whitespace를 제거한다. 테스트는 cache 없는 명령으로 실행한다.
수정 파일 및 체크리스트:
- [ ] `apps/edge/internal/service/service.go`, `apps/edge/internal/service/service_test.go`, `apps/edge/internal/controlplane/connector.go`, `apps/edge/internal/controlplane/connector_test.go`에 `gofmt` 적용.
- [ ] `proto/iop/control.proto` EOF blank line 정리. proto schema 자체는 불필요하면 변경하지 않는다.
- [ ] `git diff --check` 통과.
테스트 작성:
- 별도 테스트 파일 추가 없음. 위 항목들의 회귀 테스트와 diff check가 검증이다.
중간 검증:
```bash
git diff --check
```
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `apps/edge/internal/service/service.go` | REVIEW_API-1, REVIEW_API-2, REVIEW_API-4 |
| `apps/edge/internal/service/service_test.go` | REVIEW_API-1, REVIEW_API-2, REVIEW_API-4 |
| `apps/edge/internal/controlplane/connector.go` | REVIEW_API-3, REVIEW_API-4 |
| `apps/edge/internal/controlplane/connector_test.go` | REVIEW_API-3, REVIEW_API-4 |
| `proto/iop/control.proto` | REVIEW_API-4 only if whitespace cleanup is still needed |
## 최종 검증
```bash
go test -count=1 ./apps/edge/internal/service ./apps/edge/internal/controlplane
go test -count=1 ./apps/edge/...
make test-control-plane-edge-wire
git diff --check
```
예상 결과: 모든 명령 exit code 0. `go test` cache output은 완료 증거로 쓰지 말고 `-count=1` fresh output을 기록한다.
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -0,0 +1,165 @@
<!-- task=m-multi-edge-operations/02+01_edge_fleet_ops plan=2 tag=REVIEW_REVIEW_API -->
# Plan - REVIEW_REVIEW_API Agent Command Coverage
## 이 파일을 읽는 구현 에이전트에게
`CODE_REVIEW-cloud-G06.md`의 구현 에이전트 소유 섹션을 채우는 것이 구현의 마지막 단계다. 코드 변경, 테스트 실행, 실제 stdout/stderr 기록을 끝낸 뒤 active 파일을 그대로 두고 리뷰 준비를 보고한다. 최종 판정, log rename, `complete.log`, archive 이동은 code-review 전용이다.
구현 중 사용자만 결정할 수 있는 scope 변경, 사용자 소유 외부 환경/secret, 또는 자동 후속으로 해결할 수 없는 차단이 생기면 `CODE_REVIEW-cloud-G06.md`의 `사용자 리뷰 요청` 섹션에 근거와 재개 조건을 채우고 중단한다. 명령 재실행이나 증거 수집으로 닫을 수 있는 공백은 사용자 리뷰 요청이 아니라 구현/검증 항목으로 처리한다.
## 배경
G06 구현은 `agent.command` fake success를 제거했지만, 실제 supported command branch의 성공과 downstream node error 전파를 직접 검증하지 않는다. 이 follow-up은 `apps/edge/internal/service` 테스트만 보강해 `agent.command`가 실제 `NodeCommandRequest`를 보내는지, node error를 `EdgeCommandResponse`와 event로 반영하는지 확인한다.
## 사용자 리뷰 요청 흐름
구현 중 blocker는 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. code-review가 그 요청을 검증하고 필요할 때만 `USER_REVIEW.md`를 작성한다.
## 분석 결과
### 읽은 파일
- `agent-task/m-multi-edge-operations/02+01_edge_fleet_ops/plan_cloud_G06_1.log`
- `agent-task/m-multi-edge-operations/02+01_edge_fleet_ops/code_review_cloud_G06_1.log`
- `apps/edge/internal/service/service.go`
- `apps/edge/internal/service/service_test.go`
- `apps/edge/internal/controlplane/connector.go`
- `apps/edge/internal/controlplane/connector_test.go`
- `agent-test/local/rules.md`
- `agent-test/local/edge-smoke.md`
### 테스트 환경 규칙
- test_env: `local`
- matched profile: `agent-test/local/edge-smoke.md`
- applied commands: service package fresh test, edge package fresh test, `git diff --check`
- no external secret, Docker, Flutter, or remote runner is required for this follow-up.
### 테스트 커버리지 공백
- Missing: connected-node `agent.command` + `parameters.command=capabilities` success path sends a real `NodeCommandRequest`.
- Missing: supported command downstream `NodeCommandResponse.Error` becomes `EdgeCommandResponse.Status="error"` plus failed event.
- Already covered: nil client no longer fakes success, lifecycle summary states, status config non-leak, command relay parser.
### 심볼 참조
- Renamed/removed symbols: none.
### 분할 판단
- Existing split subtask remains `m-multi-edge-operations/02+01_edge_fleet_ops`.
- No additional split: this is one test-coverage repair inside `apps/edge/internal/service`.
### 범위 결정 근거
- Do not change proto schema, Control Plane API, or client UI.
- Do not add broad E2E harness. A package-level fake proto-socket node or injectable test helper is enough.
- Production code changes are allowed only if needed to make the command path testable without weakening behavior.
### 빌드 등급
- build=`cloud-G06`, review=`cloud-G06`: this is protocol/transport-path coverage for a prior Required issue, and the same cloud route remains appropriate.
## 구현 체크리스트
- [ ] [REVIEW_REVIEW_API-1] `agent.command` capabilities success path가 실제 `NodeCommandRequest`를 보내고 completed response/event를 반환하는 테스트를 추가한다.
- [ ] [REVIEW_REVIEW_API-2] `agent.command` capabilities downstream node error가 error response와 failed event로 전파되는 테스트를 추가한다.
- [ ] [REVIEW_REVIEW_API-3] fresh service/edge tests와 `git diff --check`를 실행하고 결과를 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
### [REVIEW_REVIEW_API-1] Supported Command Success Test
문제:
`apps/edge/internal/service/service.go:770`의 `agent.command` capabilities branch는 code상 `s.Capabilities`를 호출하지만, 현재 `apps/edge/internal/service/service_test.go:522`는 nil client error만 검증한다.
해결 방법:
`service_test`에 fake proto-socket node client를 구성하거나, 기존 service 구조에 맞는 최소 test helper를 만든다. `ExecuteCommand`에 `Operation: "agent.command"`, `TargetSelector: "deployer"`, `Parameters: {"command":"capabilities","adapter":"cli","target":"codex"}`를 보내고 fake node가 받은 `NodeCommandRequest.Type == NODE_COMMAND_TYPE_CAPABILITIES`를 assert한다. fake node가 `Result`를 반환하면 response는 `Status: "completed"`이고 event sequence는 `started`, `completed`여야 한다.
수정 파일 및 체크리스트:
- [ ] `apps/edge/internal/service/service_test.go`: fake node command transport/helper 추가.
- [ ] `apps/edge/internal/service/service_test.go`: success test 추가.
- [ ] 요청의 adapter/target/session 값이 `NodeCommandRequest`로 전달되는지 검증.
테스트 작성:
- Add `TestServiceExecuteCommandAgentCommandCapabilitiesSuccess`.
중간 검증:
```bash
go test -count=1 ./apps/edge/internal/service
```
### [REVIEW_REVIEW_API-2] Downstream Error Test
문제:
`apps/edge/internal/service/service.go:639`는 downstream error를 `EdgeCommandResponse{Status:"error"}`로 변환하지만, `agent.command` 경유 node error fixture가 없다.
해결 방법:
success test의 fake node를 재사용해 `NodeCommandResponse{Error:"boom"}`를 반환하게 한다. `ExecuteCommand` 결과가 `Status: "error"`, `Error`에 `node reported error: boom`을 포함하고 마지막 event가 `Phase: "failed"`인지 확인한다.
수정 파일 및 체크리스트:
- [ ] `apps/edge/internal/service/service_test.go`: node error variant 추가.
- [ ] response error와 failed event summary를 함께 검증.
테스트 작성:
- Add `TestServiceExecuteCommandAgentCommandCapabilitiesNodeError`.
중간 검증:
```bash
go test -count=1 ./apps/edge/internal/service
```
### [REVIEW_REVIEW_API-3] Verification
문제:
이 follow-up은 테스트 신뢰 회복 작업이므로 fresh output이 필요하다.
해결 방법:
대상 package, edge 전체 package, diff cleanliness를 fresh run으로 확인한다.
수정 파일 및 체크리스트:
- [ ] `go test -count=1 ./apps/edge/internal/service`
- [ ] `go test -count=1 ./apps/edge/...`
- [ ] `git diff --check`
테스트 작성:
- 별도 테스트 없음. 위 명령 결과가 검증이다.
중간 검증:
```bash
git diff --check
```
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `apps/edge/internal/service/service_test.go` | REVIEW_REVIEW_API-1, REVIEW_REVIEW_API-2, REVIEW_REVIEW_API-3 |
| `apps/edge/internal/service/service.go` | REVIEW_REVIEW_API-1, REVIEW_REVIEW_API-2 only if testability requires a small helper |
## 최종 검증
```bash
go test -count=1 ./apps/edge/internal/service
go test -count=1 ./apps/edge/...
git diff --check
```
예상 결과: 모든 명령 exit code 0. `go test` cache output은 완료 증거로 쓰지 말고 `-count=1` fresh output을 기록한다.
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -0,0 +1,138 @@
<!-- task=m-multi-edge-operations/02+01_edge_fleet_ops plan=3 tag=REVIEW_REVIEW_REVIEW_API -->
# Plan - REVIEW_REVIEW_REVIEW_API Session Propagation Coverage
## 이 파일을 읽는 구현 에이전트에게
`CODE_REVIEW-cloud-G06.md`의 구현 에이전트 소유 섹션을 채우는 것이 구현의 마지막 단계다. 코드 변경, 테스트 실행, 실제 stdout/stderr 기록을 끝낸 뒤 active 파일을 그대로 두고 리뷰 준비를 보고한다. 최종 판정, log rename, `complete.log`, archive 이동은 code-review 전용이다.
구현 중 사용자만 결정할 수 있는 scope 변경, 사용자 소유 외부 환경/secret, 또는 자동 후속으로 해결할 수 없는 차단이 생기면 `CODE_REVIEW-cloud-G06.md`의 `사용자 리뷰 요청` 섹션에 근거와 재개 조건을 채우고 중단한다. 명령 재실행이나 증거 수집으로 닫을 수 있는 공백은 사용자 리뷰 요청이 아니라 구현/검증 항목으로 처리한다.
## 배경
G06 plan=2 follow-up은 connected-node `agent.command` + `parameters.command=capabilities` success path가 실제 `NodeCommandRequest`를 보내는지 검증하도록 요구했다. 현재 테스트는 `NodeCommandRequest.Type`, `Adapter`, `Target`은 확인하지만 explicit `session_id`를 보내지 않고 `NodeCommandRequest.SessionId`도 assert하지 않는다.
이 후속은 기존 success round-trip 테스트의 session propagation만 보강한다. 새 프로토콜, Control Plane API, connector relay, UI 변경은 범위 밖이다.
## 사용자 리뷰 요청 흐름
구현 중 blocker는 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. code-review가 그 요청을 검증하고 필요할 때만 `USER_REVIEW.md`를 작성한다.
## 분석 결과
### 읽은 파일
- `agent-task/m-multi-edge-operations/02+01_edge_fleet_ops/plan_cloud_G06_2.log`
- `agent-task/m-multi-edge-operations/02+01_edge_fleet_ops/code_review_cloud_G06_2.log`
- `apps/edge/internal/service/service.go`
- `apps/edge/internal/service/service_test.go`
- `agent-test/local/rules.md`
- `agent-test/local/edge-smoke.md`
### 테스트 환경 규칙
- test_env: `local`
- matched profile: `agent-test/local/edge-smoke.md`
- applied commands: service package fresh test, edge package fresh test, `git diff --check`
- no external secret, Docker, Flutter, or remote runner is required for this follow-up.
### 테스트 커버리지 공백
- Missing: `TestServiceExecuteCommandAgentCommandCapabilitiesSuccess` does not send `parameters["session_id"]` and does not assert `NodeCommandRequest.SessionId`.
- Already covered: real `NodeCommandRequest.Type == NODE_COMMAND_TYPE_CAPABILITIES`, adapter/target propagation, completed response/event, downstream node error response and failed event.
### 심볼 참조
- Renamed/removed symbols: none.
### 분할 판단
- Existing split subtask remains `m-multi-edge-operations/02+01_edge_fleet_ops`.
- No additional split: this is one test-coverage repair inside `apps/edge/internal/service`.
### 범위 결정 근거
- Do not change proto schema, Control Plane API, connector relay, or client UI.
- Prefer a test-only change: add an explicit `session_id` parameter to the existing success test and assert `req.GetSessionId()`.
- Production code change is allowed only if the new assertion exposes a real mapping bug in `ExecuteCommand`.
### 빌드 등급
- build=`cloud-G06`, review=`cloud-G06`: this remains a protocol-path coverage repair for a repeated Required issue in the same cloud review lane.
## 구현 체크리스트
- [ ] [REVIEW_REVIEW_REVIEW_API-1] `agent.command` capabilities success test가 explicit `session_id`를 보내고 실제 `NodeCommandRequest.SessionId` 전달을 검증하도록 보강한다.
- [ ] [REVIEW_REVIEW_REVIEW_API-2] fresh service/edge tests와 `git diff --check`를 실행하고 결과를 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
### [REVIEW_REVIEW_REVIEW_API-1] Explicit Session Propagation Test
문제:
`apps/edge/internal/service/service_test.go:604`의 node request listener는 type/adapter/target만 검증한다. `apps/edge/internal/service/service_test.go:636`의 request parameters도 `session_id`를 보내지 않아, `apps/edge/internal/service/service.go:775`의 `session_id` bridge가 깨져도 테스트가 통과한다.
해결 방법:
기존 `TestServiceExecuteCommandAgentCommandCapabilitiesSuccess`를 수정한다. `EdgeCommandRequest.Parameters`에 예를 들어 `"session_id": "session-a"`를 추가하고, fake node listener에서 `req.GetSessionId() == "session-a"`를 assert한다. 현재 구현이 이미 맞다면 service production code는 변경하지 않는다. 새 assertion이 실패하면 `ExecuteCommand`의 `NodeCommandRequestSpec.SessionID` mapping만 고친다.
수정 파일 및 체크리스트:
- [ ] `apps/edge/internal/service/service_test.go`: success request parameters에 explicit `session_id` 추가.
- [ ] `apps/edge/internal/service/service_test.go`: fake node listener에서 `req.GetSessionId()` assertion 추가.
- [ ] `apps/edge/internal/service/service.go`: 필요할 때만 `parameters["session_id"]` propagation 수정.
테스트 작성:
- Update `TestServiceExecuteCommandAgentCommandCapabilitiesSuccess`.
중간 검증:
```bash
go test -count=1 ./apps/edge/internal/service
```
### [REVIEW_REVIEW_REVIEW_API-2] Verification
문제:
이 follow-up은 테스트 신뢰 회복 작업이므로 fresh output이 필요하다.
해결 방법:
대상 package, edge 전체 package, diff cleanliness를 fresh run으로 확인한다.
수정 파일 및 체크리스트:
- [ ] `go test -count=1 ./apps/edge/internal/service`
- [ ] `go test -count=1 ./apps/edge/...`
- [ ] `git diff --check`
테스트 작성:
- 별도 테스트 없음. 위 명령 결과가 검증이다.
중간 검증:
```bash
git diff --check
```
## 수정 파일 요약
| 파일 | 항목 |
|------|------|
| `apps/edge/internal/service/service_test.go` | REVIEW_REVIEW_REVIEW_API-1, REVIEW_REVIEW_REVIEW_API-2 |
| `apps/edge/internal/service/service.go` | REVIEW_REVIEW_REVIEW_API-1 only if the new assertion exposes a production mapping bug |
## 최종 검증
```bash
go test -count=1 ./apps/edge/internal/service
go test -count=1 ./apps/edge/...
git diff --check
```
예상 결과: 모든 명령 exit code 0. `go test` cache output은 완료 증거로 쓰지 말고 `-count=1` fresh output을 기록한다.
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.

View file

@ -1,39 +0,0 @@
<!-- task=m-multi-edge-operations/01_native_contract review=0 tag=API -->
# Code Review - Native Fleet Contract
## Review Scope
- `proto/iop/control.proto`
- proto generated files
- `apps/control-plane/internal/wire/edge.go`
- `apps/edge/internal/controlplane/connector.go`
- ownership guard tests
## Roadmap Targets
- `agent-roadmap/phase/control-plane-portal-ops/milestones/multi-edge-operations.md` `[native-fleet]`
- `agent-roadmap/phase/control-plane-portal-ops/milestones/multi-edge-operations.md` `[edge-ownership]`
## Reviewer Checklist
- [ ] Control Plane이 Edge 설정, Node registry, runtime/automation state 원본을 소유하는 필드나 저장소가 생기지 않았다.
- [ ] OTO/build-deploy는 capability/status/command summary로만 표현된다.
- [ ] command 요청/응답/event relay 메시지가 request/result 추적에 충분하다.
- [ ] parser map이 양쪽에서 대칭적으로 갱신되었다.
- [ ] proto 생성물이 누락되지 않았다.
- [ ] 실패/unknown enum 처리 테스트가 있다.
## Verification Result
- [ ] `make proto`
- [ ] `make proto-dart`
- [ ] `go test ./apps/control-plane/internal/wire ./apps/edge/internal/controlplane`
- [ ] 필요한 경우 `cd apps/client && flutter test`
## User Review
검토 기본값: 이 리뷰는 사용자가 명시적으로 보류하지 않으면 구현자가 자체 검증 후 완료 처리한다.
## Notes
- 리뷰 중 발견한 계약 변경은 후속 plan의 구현 전제에 영향을 주므로 milestone 작업 맥락에 함께 반영한다.

View file

@ -1,30 +0,0 @@
<!-- task=m-multi-edge-operations/02+01_edge_fleet_ops review=0 tag=API -->
# Code Review - Edge Fleet Operations
## Review Scope
- `apps/edge/internal/controlplane/connector.go`
- `apps/edge/internal/service/service.go`
- Edge connector/service tests
## Dependency Check
- [ ] `agent-task/m-multi-edge-operations/01_native_contract/complete.log` 확인
## Reviewer Checklist
- [ ] command 처리가 Edge-local service 책임으로 남아 있다.
- [ ] Control Plane 연결/재연결이 Edge 설정 원본을 변경하지 않는다.
- [ ] OTO/build-deploy 정보는 summary 수준이다.
- [ ] unsupported/busy/error command 응답이 명확하다.
- [ ] event relay가 연결 실패/재연결 상황에서 안전하다.
- [ ] secret/config detail이 status response로 새지 않는다.
## Verification Result
- [ ] `go test ./apps/edge/internal/service ./apps/edge/internal/controlplane`
- [ ] 필요한 경우 `go test ./apps/edge/...`
## User Review
검토 기본값: 이 리뷰는 사용자가 명시적으로 보류하지 않으면 구현자가 자체 검증 후 완료 처리한다.

View file

@ -797,6 +797,8 @@ class EdgeStatusResponse extends $pb.GeneratedMessage {
$core.Iterable<EdgeNodeSnapshot>? nodes,
$core.Iterable<$core.MapEntry<$core.String, $core.String>>? metadata,
$core.String? error,
$core.Iterable<EdgeCapabilitySummary>? capabilities,
$core.Iterable<EdgeDomainAgentSummary>? domainAgents,
}) {
final result = create();
if (requestId != null) result.requestId = requestId;
@ -807,6 +809,8 @@ class EdgeStatusResponse extends $pb.GeneratedMessage {
if (nodes != null) result.nodes.addAll(nodes);
if (metadata != null) result.metadata.addEntries(metadata);
if (error != null) result.error = error;
if (capabilities != null) result.capabilities.addAll(capabilities);
if (domainAgents != null) result.domainAgents.addAll(domainAgents);
return result;
}
@ -835,6 +839,10 @@ class EdgeStatusResponse extends $pb.GeneratedMessage {
valueFieldType: $pb.PbFieldType.OS,
packageName: const $pb.PackageName('iop'))
..aOS(7, _omitFieldNames ? '' : 'error')
..pPM<EdgeCapabilitySummary>(8, _omitFieldNames ? '' : 'capabilities',
subBuilder: EdgeCapabilitySummary.create)
..pPM<EdgeDomainAgentSummary>(9, _omitFieldNames ? '' : 'domainAgents',
subBuilder: EdgeDomainAgentSummary.create)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
@ -906,6 +914,522 @@ class EdgeStatusResponse extends $pb.GeneratedMessage {
$core.bool hasError() => $_has(6);
@$pb.TagNumber(7)
void clearError() => $_clearField(7);
@$pb.TagNumber(8)
$pb.PbList<EdgeCapabilitySummary> get capabilities => $_getList(7);
@$pb.TagNumber(9)
$pb.PbList<EdgeDomainAgentSummary> get domainAgents => $_getList(8);
}
class EdgeCapabilitySummary extends $pb.GeneratedMessage {
factory EdgeCapabilitySummary({
$core.String? kind,
$core.bool? available,
$core.String? status,
$core.String? summary,
}) {
final result = create();
if (kind != null) result.kind = kind;
if (available != null) result.available = available;
if (status != null) result.status = status;
if (summary != null) result.summary = summary;
return result;
}
EdgeCapabilitySummary._();
factory EdgeCapabilitySummary.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory EdgeCapabilitySummary.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'EdgeCapabilitySummary',
package: const $pb.PackageName(_omitMessageNames ? '' : 'iop'),
createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'kind')
..aOB(2, _omitFieldNames ? '' : 'available')
..aOS(3, _omitFieldNames ? '' : 'status')
..aOS(4, _omitFieldNames ? '' : 'summary')
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
EdgeCapabilitySummary clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
EdgeCapabilitySummary copyWith(
void Function(EdgeCapabilitySummary) updates) =>
super.copyWith((message) => updates(message as EdgeCapabilitySummary))
as EdgeCapabilitySummary;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static EdgeCapabilitySummary create() => EdgeCapabilitySummary._();
@$core.override
EdgeCapabilitySummary createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static EdgeCapabilitySummary getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<EdgeCapabilitySummary>(create);
static EdgeCapabilitySummary? _defaultInstance;
@$pb.TagNumber(1)
$core.String get kind => $_getSZ(0);
@$pb.TagNumber(1)
set kind($core.String value) => $_setString(0, value);
@$pb.TagNumber(1)
$core.bool hasKind() => $_has(0);
@$pb.TagNumber(1)
void clearKind() => $_clearField(1);
@$pb.TagNumber(2)
$core.bool get available => $_getBF(1);
@$pb.TagNumber(2)
set available($core.bool value) => $_setBool(1, value);
@$pb.TagNumber(2)
$core.bool hasAvailable() => $_has(1);
@$pb.TagNumber(2)
void clearAvailable() => $_clearField(2);
@$pb.TagNumber(3)
$core.String get status => $_getSZ(2);
@$pb.TagNumber(3)
set status($core.String value) => $_setString(2, value);
@$pb.TagNumber(3)
$core.bool hasStatus() => $_has(2);
@$pb.TagNumber(3)
void clearStatus() => $_clearField(3);
@$pb.TagNumber(4)
$core.String get summary => $_getSZ(3);
@$pb.TagNumber(4)
set summary($core.String value) => $_setString(3, value);
@$pb.TagNumber(4)
$core.bool hasSummary() => $_has(3);
@$pb.TagNumber(4)
void clearSummary() => $_clearField(4);
}
class EdgeDomainAgentSummary extends $pb.GeneratedMessage {
factory EdgeDomainAgentSummary({
$core.String? agentKind,
$core.bool? available,
$core.String? lifecycleState,
$core.String? activeCommandId,
$core.String? summary,
}) {
final result = create();
if (agentKind != null) result.agentKind = agentKind;
if (available != null) result.available = available;
if (lifecycleState != null) result.lifecycleState = lifecycleState;
if (activeCommandId != null) result.activeCommandId = activeCommandId;
if (summary != null) result.summary = summary;
return result;
}
EdgeDomainAgentSummary._();
factory EdgeDomainAgentSummary.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory EdgeDomainAgentSummary.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'EdgeDomainAgentSummary',
package: const $pb.PackageName(_omitMessageNames ? '' : 'iop'),
createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'agentKind')
..aOB(2, _omitFieldNames ? '' : 'available')
..aOS(3, _omitFieldNames ? '' : 'lifecycleState')
..aOS(4, _omitFieldNames ? '' : 'activeCommandId')
..aOS(5, _omitFieldNames ? '' : 'summary')
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
EdgeDomainAgentSummary clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
EdgeDomainAgentSummary copyWith(
void Function(EdgeDomainAgentSummary) updates) =>
super.copyWith((message) => updates(message as EdgeDomainAgentSummary))
as EdgeDomainAgentSummary;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static EdgeDomainAgentSummary create() => EdgeDomainAgentSummary._();
@$core.override
EdgeDomainAgentSummary createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static EdgeDomainAgentSummary getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<EdgeDomainAgentSummary>(create);
static EdgeDomainAgentSummary? _defaultInstance;
@$pb.TagNumber(1)
$core.String get agentKind => $_getSZ(0);
@$pb.TagNumber(1)
set agentKind($core.String value) => $_setString(0, value);
@$pb.TagNumber(1)
$core.bool hasAgentKind() => $_has(0);
@$pb.TagNumber(1)
void clearAgentKind() => $_clearField(1);
@$pb.TagNumber(2)
$core.bool get available => $_getBF(1);
@$pb.TagNumber(2)
set available($core.bool value) => $_setBool(1, value);
@$pb.TagNumber(2)
$core.bool hasAvailable() => $_has(1);
@$pb.TagNumber(2)
void clearAvailable() => $_clearField(2);
@$pb.TagNumber(3)
$core.String get lifecycleState => $_getSZ(2);
@$pb.TagNumber(3)
set lifecycleState($core.String value) => $_setString(2, value);
@$pb.TagNumber(3)
$core.bool hasLifecycleState() => $_has(2);
@$pb.TagNumber(3)
void clearLifecycleState() => $_clearField(3);
@$pb.TagNumber(4)
$core.String get activeCommandId => $_getSZ(3);
@$pb.TagNumber(4)
set activeCommandId($core.String value) => $_setString(3, value);
@$pb.TagNumber(4)
$core.bool hasActiveCommandId() => $_has(3);
@$pb.TagNumber(4)
void clearActiveCommandId() => $_clearField(4);
@$pb.TagNumber(5)
$core.String get summary => $_getSZ(4);
@$pb.TagNumber(5)
set summary($core.String value) => $_setString(4, value);
@$pb.TagNumber(5)
$core.bool hasSummary() => $_has(4);
@$pb.TagNumber(5)
void clearSummary() => $_clearField(5);
}
class EdgeCommandRequest extends $pb.GeneratedMessage {
factory EdgeCommandRequest({
$core.String? requestId,
$core.String? commandId,
$core.String? targetSelector,
$core.String? operation,
$core.Iterable<$core.MapEntry<$core.String, $core.String>>? parameters,
}) {
final result = create();
if (requestId != null) result.requestId = requestId;
if (commandId != null) result.commandId = commandId;
if (targetSelector != null) result.targetSelector = targetSelector;
if (operation != null) result.operation = operation;
if (parameters != null) result.parameters.addEntries(parameters);
return result;
}
EdgeCommandRequest._();
factory EdgeCommandRequest.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory EdgeCommandRequest.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'EdgeCommandRequest',
package: const $pb.PackageName(_omitMessageNames ? '' : 'iop'),
createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'requestId')
..aOS(2, _omitFieldNames ? '' : 'commandId')
..aOS(3, _omitFieldNames ? '' : 'targetSelector')
..aOS(4, _omitFieldNames ? '' : 'operation')
..m<$core.String, $core.String>(5, _omitFieldNames ? '' : 'parameters',
entryClassName: 'EdgeCommandRequest.ParametersEntry',
keyFieldType: $pb.PbFieldType.OS,
valueFieldType: $pb.PbFieldType.OS,
packageName: const $pb.PackageName('iop'))
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
EdgeCommandRequest clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
EdgeCommandRequest copyWith(void Function(EdgeCommandRequest) updates) =>
super.copyWith((message) => updates(message as EdgeCommandRequest))
as EdgeCommandRequest;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static EdgeCommandRequest create() => EdgeCommandRequest._();
@$core.override
EdgeCommandRequest createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static EdgeCommandRequest getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<EdgeCommandRequest>(create);
static EdgeCommandRequest? _defaultInstance;
@$pb.TagNumber(1)
$core.String get requestId => $_getSZ(0);
@$pb.TagNumber(1)
set requestId($core.String value) => $_setString(0, value);
@$pb.TagNumber(1)
$core.bool hasRequestId() => $_has(0);
@$pb.TagNumber(1)
void clearRequestId() => $_clearField(1);
@$pb.TagNumber(2)
$core.String get commandId => $_getSZ(1);
@$pb.TagNumber(2)
set commandId($core.String value) => $_setString(1, value);
@$pb.TagNumber(2)
$core.bool hasCommandId() => $_has(1);
@$pb.TagNumber(2)
void clearCommandId() => $_clearField(2);
@$pb.TagNumber(3)
$core.String get targetSelector => $_getSZ(2);
@$pb.TagNumber(3)
set targetSelector($core.String value) => $_setString(2, value);
@$pb.TagNumber(3)
$core.bool hasTargetSelector() => $_has(2);
@$pb.TagNumber(3)
void clearTargetSelector() => $_clearField(3);
@$pb.TagNumber(4)
$core.String get operation => $_getSZ(3);
@$pb.TagNumber(4)
set operation($core.String value) => $_setString(3, value);
@$pb.TagNumber(4)
$core.bool hasOperation() => $_has(3);
@$pb.TagNumber(4)
void clearOperation() => $_clearField(4);
@$pb.TagNumber(5)
$pb.PbMap<$core.String, $core.String> get parameters => $_getMap(4);
}
class EdgeCommandResponse extends $pb.GeneratedMessage {
factory EdgeCommandResponse({
$core.String? requestId,
$core.String? commandId,
$core.String? edgeId,
$core.String? status,
$core.String? summary,
$core.String? error,
}) {
final result = create();
if (requestId != null) result.requestId = requestId;
if (commandId != null) result.commandId = commandId;
if (edgeId != null) result.edgeId = edgeId;
if (status != null) result.status = status;
if (summary != null) result.summary = summary;
if (error != null) result.error = error;
return result;
}
EdgeCommandResponse._();
factory EdgeCommandResponse.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory EdgeCommandResponse.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'EdgeCommandResponse',
package: const $pb.PackageName(_omitMessageNames ? '' : 'iop'),
createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'requestId')
..aOS(2, _omitFieldNames ? '' : 'commandId')
..aOS(3, _omitFieldNames ? '' : 'edgeId')
..aOS(4, _omitFieldNames ? '' : 'status')
..aOS(5, _omitFieldNames ? '' : 'summary')
..aOS(6, _omitFieldNames ? '' : 'error')
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
EdgeCommandResponse clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
EdgeCommandResponse copyWith(void Function(EdgeCommandResponse) updates) =>
super.copyWith((message) => updates(message as EdgeCommandResponse))
as EdgeCommandResponse;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static EdgeCommandResponse create() => EdgeCommandResponse._();
@$core.override
EdgeCommandResponse createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static EdgeCommandResponse getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<EdgeCommandResponse>(create);
static EdgeCommandResponse? _defaultInstance;
@$pb.TagNumber(1)
$core.String get requestId => $_getSZ(0);
@$pb.TagNumber(1)
set requestId($core.String value) => $_setString(0, value);
@$pb.TagNumber(1)
$core.bool hasRequestId() => $_has(0);
@$pb.TagNumber(1)
void clearRequestId() => $_clearField(1);
@$pb.TagNumber(2)
$core.String get commandId => $_getSZ(1);
@$pb.TagNumber(2)
set commandId($core.String value) => $_setString(1, value);
@$pb.TagNumber(2)
$core.bool hasCommandId() => $_has(1);
@$pb.TagNumber(2)
void clearCommandId() => $_clearField(2);
@$pb.TagNumber(3)
$core.String get edgeId => $_getSZ(2);
@$pb.TagNumber(3)
set edgeId($core.String value) => $_setString(2, value);
@$pb.TagNumber(3)
$core.bool hasEdgeId() => $_has(2);
@$pb.TagNumber(3)
void clearEdgeId() => $_clearField(3);
@$pb.TagNumber(4)
$core.String get status => $_getSZ(3);
@$pb.TagNumber(4)
set status($core.String value) => $_setString(3, value);
@$pb.TagNumber(4)
$core.bool hasStatus() => $_has(3);
@$pb.TagNumber(4)
void clearStatus() => $_clearField(4);
@$pb.TagNumber(5)
$core.String get summary => $_getSZ(4);
@$pb.TagNumber(5)
set summary($core.String value) => $_setString(4, value);
@$pb.TagNumber(5)
$core.bool hasSummary() => $_has(4);
@$pb.TagNumber(5)
void clearSummary() => $_clearField(5);
@$pb.TagNumber(6)
$core.String get error => $_getSZ(5);
@$pb.TagNumber(6)
set error($core.String value) => $_setString(5, value);
@$pb.TagNumber(6)
$core.bool hasError() => $_has(5);
@$pb.TagNumber(6)
void clearError() => $_clearField(6);
}
class EdgeCommandEvent extends $pb.GeneratedMessage {
factory EdgeCommandEvent({
$core.String? commandId,
$core.String? edgeId,
$core.String? phase,
$core.String? summary,
$fixnum.Int64? occurredAt,
}) {
final result = create();
if (commandId != null) result.commandId = commandId;
if (edgeId != null) result.edgeId = edgeId;
if (phase != null) result.phase = phase;
if (summary != null) result.summary = summary;
if (occurredAt != null) result.occurredAt = occurredAt;
return result;
}
EdgeCommandEvent._();
factory EdgeCommandEvent.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory EdgeCommandEvent.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'EdgeCommandEvent',
package: const $pb.PackageName(_omitMessageNames ? '' : 'iop'),
createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'commandId')
..aOS(2, _omitFieldNames ? '' : 'edgeId')
..aOS(3, _omitFieldNames ? '' : 'phase')
..aOS(4, _omitFieldNames ? '' : 'summary')
..aInt64(5, _omitFieldNames ? '' : 'occurredAt')
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
EdgeCommandEvent clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
EdgeCommandEvent copyWith(void Function(EdgeCommandEvent) updates) =>
super.copyWith((message) => updates(message as EdgeCommandEvent))
as EdgeCommandEvent;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static EdgeCommandEvent create() => EdgeCommandEvent._();
@$core.override
EdgeCommandEvent createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static EdgeCommandEvent getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<EdgeCommandEvent>(create);
static EdgeCommandEvent? _defaultInstance;
@$pb.TagNumber(1)
$core.String get commandId => $_getSZ(0);
@$pb.TagNumber(1)
set commandId($core.String value) => $_setString(0, value);
@$pb.TagNumber(1)
$core.bool hasCommandId() => $_has(0);
@$pb.TagNumber(1)
void clearCommandId() => $_clearField(1);
@$pb.TagNumber(2)
$core.String get edgeId => $_getSZ(1);
@$pb.TagNumber(2)
set edgeId($core.String value) => $_setString(1, value);
@$pb.TagNumber(2)
$core.bool hasEdgeId() => $_has(1);
@$pb.TagNumber(2)
void clearEdgeId() => $_clearField(2);
@$pb.TagNumber(3)
$core.String get phase => $_getSZ(2);
@$pb.TagNumber(3)
set phase($core.String value) => $_setString(2, value);
@$pb.TagNumber(3)
$core.bool hasPhase() => $_has(2);
@$pb.TagNumber(3)
void clearPhase() => $_clearField(3);
@$pb.TagNumber(4)
$core.String get summary => $_getSZ(3);
@$pb.TagNumber(4)
set summary($core.String value) => $_setString(3, value);
@$pb.TagNumber(4)
$core.bool hasSummary() => $_has(3);
@$pb.TagNumber(4)
void clearSummary() => $_clearField(4);
@$pb.TagNumber(5)
$fixnum.Int64 get occurredAt => $_getI64(4);
@$pb.TagNumber(5)
set occurredAt($fixnum.Int64 value) => $_setInt64(4, value);
@$pb.TagNumber(5)
$core.bool hasOccurredAt() => $_has(4);
@$pb.TagNumber(5)
void clearOccurredAt() => $_clearField(5);
}
const $core.bool _omitFieldNames =

View file

@ -277,6 +277,22 @@ const EdgeStatusResponse$json = {
'10': 'metadata'
},
{'1': 'error', '3': 7, '4': 1, '5': 9, '10': 'error'},
{
'1': 'capabilities',
'3': 8,
'4': 3,
'5': 11,
'6': '.iop.EdgeCapabilitySummary',
'10': 'capabilities'
},
{
'1': 'domain_agents',
'3': 9,
'4': 3,
'5': 11,
'6': '.iop.EdgeDomainAgentSummary',
'10': 'domainAgents'
},
],
'3': [EdgeStatusResponse_MetadataEntry$json],
};
@ -298,5 +314,120 @@ final $typed_data.Uint8List edgeStatusResponseDescriptor = $convert.base64Decode
'c2VydmVkX3RpbWVfdW5peF9uYW5vGAQgASgDUhRvYnNlcnZlZFRpbWVVbml4TmFubxIrCgVub2'
'RlcxgFIAMoCzIVLmlvcC5FZGdlTm9kZVNuYXBzaG90UgVub2RlcxJBCghtZXRhZGF0YRgGIAMo'
'CzIlLmlvcC5FZGdlU3RhdHVzUmVzcG9uc2UuTWV0YWRhdGFFbnRyeVIIbWV0YWRhdGESFAoFZX'
'Jyb3IYByABKAlSBWVycm9yGjsKDU1ldGFkYXRhRW50cnkSEAoDa2V5GAEgASgJUgNrZXkSFAoF'
'dmFsdWUYAiABKAlSBXZhbHVlOgI4AQ==');
'Jyb3IYByABKAlSBWVycm9yEj4KDGNhcGFiaWxpdGllcxgIIAMoCzIaLmlvcC5FZGdlQ2FwYWJp'
'bGl0eVN1bW1hcnlSDGNhcGFiaWxpdGllcxJACg1kb21haW5fYWdlbnRzGAkgAygLMhsuaW9wLk'
'VkZ2VEb21haW5BZ2VudFN1bW1hcnlSDGRvbWFpbkFnZW50cxo7Cg1NZXRhZGF0YUVudHJ5EhAK'
'A2tleRgBIAEoCVIDa2V5EhQKBXZhbHVlGAIgASgJUgV2YWx1ZToCOAE=');
@$core.Deprecated('Use edgeCapabilitySummaryDescriptor instead')
const EdgeCapabilitySummary$json = {
'1': 'EdgeCapabilitySummary',
'2': [
{'1': 'kind', '3': 1, '4': 1, '5': 9, '10': 'kind'},
{'1': 'available', '3': 2, '4': 1, '5': 8, '10': 'available'},
{'1': 'status', '3': 3, '4': 1, '5': 9, '10': 'status'},
{'1': 'summary', '3': 4, '4': 1, '5': 9, '10': 'summary'},
],
};
/// Descriptor for `EdgeCapabilitySummary`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List edgeCapabilitySummaryDescriptor = $convert.base64Decode(
'ChVFZGdlQ2FwYWJpbGl0eVN1bW1hcnkSEgoEa2luZBgBIAEoCVIEa2luZBIcCglhdmFpbGFibG'
'UYAiABKAhSCWF2YWlsYWJsZRIWCgZzdGF0dXMYAyABKAlSBnN0YXR1cxIYCgdzdW1tYXJ5GAQg'
'ASgJUgdzdW1tYXJ5');
@$core.Deprecated('Use edgeDomainAgentSummaryDescriptor instead')
const EdgeDomainAgentSummary$json = {
'1': 'EdgeDomainAgentSummary',
'2': [
{'1': 'agent_kind', '3': 1, '4': 1, '5': 9, '10': 'agentKind'},
{'1': 'available', '3': 2, '4': 1, '5': 8, '10': 'available'},
{'1': 'lifecycle_state', '3': 3, '4': 1, '5': 9, '10': 'lifecycleState'},
{'1': 'active_command_id', '3': 4, '4': 1, '5': 9, '10': 'activeCommandId'},
{'1': 'summary', '3': 5, '4': 1, '5': 9, '10': 'summary'},
],
};
/// Descriptor for `EdgeDomainAgentSummary`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List edgeDomainAgentSummaryDescriptor = $convert.base64Decode(
'ChZFZGdlRG9tYWluQWdlbnRTdW1tYXJ5Eh0KCmFnZW50X2tpbmQYASABKAlSCWFnZW50S2luZB'
'IcCglhdmFpbGFibGUYAiABKAhSCWF2YWlsYWJsZRInCg9saWZlY3ljbGVfc3RhdGUYAyABKAlS'
'DmxpZmVjeWNsZVN0YXRlEioKEWFjdGl2ZV9jb21tYW5kX2lkGAQgASgJUg9hY3RpdmVDb21tYW'
'5kSWQSGAoHc3VtbWFyeRgFIAEoCVIHc3VtbWFyeQ==');
@$core.Deprecated('Use edgeCommandRequestDescriptor instead')
const EdgeCommandRequest$json = {
'1': 'EdgeCommandRequest',
'2': [
{'1': 'request_id', '3': 1, '4': 1, '5': 9, '10': 'requestId'},
{'1': 'command_id', '3': 2, '4': 1, '5': 9, '10': 'commandId'},
{'1': 'target_selector', '3': 3, '4': 1, '5': 9, '10': 'targetSelector'},
{'1': 'operation', '3': 4, '4': 1, '5': 9, '10': 'operation'},
{
'1': 'parameters',
'3': 5,
'4': 3,
'5': 11,
'6': '.iop.EdgeCommandRequest.ParametersEntry',
'10': 'parameters'
},
],
'3': [EdgeCommandRequest_ParametersEntry$json],
};
@$core.Deprecated('Use edgeCommandRequestDescriptor instead')
const EdgeCommandRequest_ParametersEntry$json = {
'1': 'ParametersEntry',
'2': [
{'1': 'key', '3': 1, '4': 1, '5': 9, '10': 'key'},
{'1': 'value', '3': 2, '4': 1, '5': 9, '10': 'value'},
],
'7': {'7': true},
};
/// Descriptor for `EdgeCommandRequest`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List edgeCommandRequestDescriptor = $convert.base64Decode(
'ChJFZGdlQ29tbWFuZFJlcXVlc3QSHQoKcmVxdWVzdF9pZBgBIAEoCVIJcmVxdWVzdElkEh0KCm'
'NvbW1hbmRfaWQYAiABKAlSCWNvbW1hbmRJZBInCg90YXJnZXRfc2VsZWN0b3IYAyABKAlSDnRh'
'cmdldFNlbGVjdG9yEhwKCW9wZXJhdGlvbhgEIAEoCVIJb3BlcmF0aW9uEkcKCnBhcmFtZXRlcn'
'MYBSADKAsyJy5pb3AuRWRnZUNvbW1hbmRSZXF1ZXN0LlBhcmFtZXRlcnNFbnRyeVIKcGFyYW1l'
'dGVycxo9Cg9QYXJhbWV0ZXJzRW50cnkSEAoDa2V5GAEgASgJUgNrZXkSFAoFdmFsdWUYAiABKA'
'lSBXZhbHVlOgI4AQ==');
@$core.Deprecated('Use edgeCommandResponseDescriptor instead')
const EdgeCommandResponse$json = {
'1': 'EdgeCommandResponse',
'2': [
{'1': 'request_id', '3': 1, '4': 1, '5': 9, '10': 'requestId'},
{'1': 'command_id', '3': 2, '4': 1, '5': 9, '10': 'commandId'},
{'1': 'edge_id', '3': 3, '4': 1, '5': 9, '10': 'edgeId'},
{'1': 'status', '3': 4, '4': 1, '5': 9, '10': 'status'},
{'1': 'summary', '3': 5, '4': 1, '5': 9, '10': 'summary'},
{'1': 'error', '3': 6, '4': 1, '5': 9, '10': 'error'},
],
};
/// Descriptor for `EdgeCommandResponse`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List edgeCommandResponseDescriptor = $convert.base64Decode(
'ChNFZGdlQ29tbWFuZFJlc3BvbnNlEh0KCnJlcXVlc3RfaWQYASABKAlSCXJlcXVlc3RJZBIdCg'
'pjb21tYW5kX2lkGAIgASgJUgljb21tYW5kSWQSFwoHZWRnZV9pZBgDIAEoCVIGZWRnZUlkEhYK'
'BnN0YXR1cxgEIAEoCVIGc3RhdHVzEhgKB3N1bW1hcnkYBSABKAlSB3N1bW1hcnkSFAoFZXJyb3'
'IYBiABKAlSBWVycm9y');
@$core.Deprecated('Use edgeCommandEventDescriptor instead')
const EdgeCommandEvent$json = {
'1': 'EdgeCommandEvent',
'2': [
{'1': 'command_id', '3': 1, '4': 1, '5': 9, '10': 'commandId'},
{'1': 'edge_id', '3': 2, '4': 1, '5': 9, '10': 'edgeId'},
{'1': 'phase', '3': 3, '4': 1, '5': 9, '10': 'phase'},
{'1': 'summary', '3': 4, '4': 1, '5': 9, '10': 'summary'},
{'1': 'occurred_at', '3': 5, '4': 1, '5': 3, '10': 'occurredAt'},
],
};
/// Descriptor for `EdgeCommandEvent`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List edgeCommandEventDescriptor = $convert.base64Decode(
'ChBFZGdlQ29tbWFuZEV2ZW50Eh0KCmNvbW1hbmRfaWQYASABKAlSCWNvbW1hbmRJZBIXCgdlZG'
'dlX2lkGAIgASgJUgZlZGdlSWQSFAoFcGhhc2UYAyABKAlSBXBoYXNlEhgKB3N1bW1hcnkYBCAB'
'KAlSB3N1bW1hcnkSHwoLb2NjdXJyZWRfYXQYBSABKANSCm9jY3VycmVkQXQ=');

View file

@ -31,5 +31,17 @@ func EdgeParserMap() proto_socket.ParserMap {
event := &iop.EdgeNodeEvent{}
return event, proto.Unmarshal(b, event)
},
proto_socket.TypeNameOf(&iop.EdgeCommandRequest{}): func(b []byte) (proto.Message, error) {
req := &iop.EdgeCommandRequest{}
return req, proto.Unmarshal(b, req)
},
proto_socket.TypeNameOf(&iop.EdgeCommandResponse{}): func(b []byte) (proto.Message, error) {
res := &iop.EdgeCommandResponse{}
return res, proto.Unmarshal(b, res)
},
proto_socket.TypeNameOf(&iop.EdgeCommandEvent{}): func(b []byte) (proto.Message, error) {
event := &iop.EdgeCommandEvent{}
return event, proto.Unmarshal(b, event)
},
}
}

View file

@ -1,11 +1,13 @@
package wire
import (
"reflect"
"testing"
"time"
proto_socket "git.toki-labs.com/toki/proto-socket/go"
iop "iop/proto/gen/iop"
"iop/packages/go/config"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
@ -270,3 +272,72 @@ func TestWireTransportBoundary(t *testing.T) {
t.Fatalf("ClientTransport = %q, want %q", ClientTransport, "proto-socket-ws")
}
}
func TestProtoOwnershipGuard(t *testing.T) {
forbiddenNames := []string{
"edge_config_source",
"node_registry_source",
"artifact_store",
"build_log_store",
}
messages := []proto.Message{
&iop.EdgeStatusRequest{},
&iop.EdgeStatusResponse{},
&iop.EdgeHelloRequest{},
&iop.EdgeHelloResponse{},
&iop.EdgeCommandRequest{},
&iop.EdgeCommandResponse{},
&iop.EdgeCommandEvent{},
&iop.EdgeCapabilitySummary{},
&iop.EdgeDomainAgentSummary{},
}
for _, msg := range messages {
fields := msg.ProtoReflect().Descriptor().Fields()
for i := 0; i < fields.Len(); i++ {
f := fields.Get(i)
name := string(f.Name())
for _, forbidden := range forbiddenNames {
if name == forbidden {
t.Errorf("Message %s contains forbidden field %s", msg.ProtoReflect().Descriptor().Name(), name)
}
}
}
}
}
func TestControlPlaneRegistryOwnershipGuard(t *testing.T) {
visited := make(map[reflect.Type]bool)
if hasEdgeConfig(reflect.TypeOf(EdgeRegistry{}), visited) {
t.Errorf("EdgeRegistry must not store EdgeConfig or configurations containing EdgeConfig")
}
visitedState := make(map[reflect.Type]bool)
if hasEdgeConfig(reflect.TypeOf(EdgeConnectionState{}), visitedState) {
t.Errorf("EdgeConnectionState must not store EdgeConfig or configurations containing EdgeConfig")
}
}
func hasEdgeConfig(t reflect.Type, visited map[reflect.Type]bool) bool {
if visited[t] {
return false
}
visited[t] = true
if t == reflect.TypeOf(config.EdgeConfig{}) {
return true
}
switch t.Kind() {
case reflect.Struct:
for i := 0; i < t.NumField(); i++ {
if hasEdgeConfig(t.Field(i).Type, visited) {
return true
}
}
case reflect.Ptr, reflect.Slice, reflect.Array:
return hasEdgeConfig(t.Elem(), visited)
case reflect.Map:
return hasEdgeConfig(t.Key(), visited) || hasEdgeConfig(t.Elem(), visited)
}
return false
}

View file

@ -44,6 +44,9 @@ const (
// the node registry or transport directly.
type StatusProvider interface {
ListNodeSnapshots() []edgeservice.NodeSnapshot
GetCapabilities() []*iop.EdgeCapabilitySummary
GetDomainAgents() []*iop.EdgeDomainAgentSummary
ExecuteCommand(ctx context.Context, req *iop.EdgeCommandRequest, onEvent func(*iop.EdgeCommandEvent)) (*iop.EdgeCommandResponse, error)
}
// Option configures optional Connector dependencies without breaking existing
@ -230,6 +233,10 @@ func (c *Connector) connect(ctx context.Context) error {
return c.buildStatusResponse(req), nil
})
toki.AddRequestListenerTyped(&cl.Communicator, func(req *iop.EdgeCommandRequest) (*iop.EdgeCommandResponse, error) {
return c.handleEdgeCommand(ctx, req)
})
c.mu.Lock()
c.client = cl
c.mu.Unlock()
@ -362,13 +369,57 @@ func (c *Connector) buildStatusResponse(req *iop.EdgeStatusRequest) *iop.EdgeSta
Alias: s.Alias,
Label: s.Label,
Connected: true,
Config: s.Config,
Config: nil,
})
}
resp.Nodes = nodes
resp.Capabilities = c.statusProvider.GetCapabilities()
resp.DomainAgents = c.statusProvider.GetDomainAgents()
return resp
}
func (c *Connector) handleEdgeCommand(ctx context.Context, req *iop.EdgeCommandRequest) (*iop.EdgeCommandResponse, error) {
c.logger.Info("received edge command request", zap.String("command_id", req.CommandId), zap.String("operation", req.Operation))
if c.statusProvider == nil {
return &iop.EdgeCommandResponse{
RequestId: req.RequestId,
CommandId: req.CommandId,
EdgeId: c.edgeInfo.ID,
Status: "error",
Error: "edge status provider not configured",
}, nil
}
onEvent := func(event *iop.EdgeCommandEvent) {
event.EdgeId = c.edgeInfo.ID
c.mu.Lock()
client := c.client
c.mu.Unlock()
if client != nil {
if err := client.Send(event); err != nil {
c.logger.Error("failed to relay edge command event", zap.Error(err))
}
}
}
resp, err := c.statusProvider.ExecuteCommand(ctx, req, onEvent)
if err != nil {
c.logger.Error("edge command execution failed", zap.Error(err))
return &iop.EdgeCommandResponse{
RequestId: req.RequestId,
CommandId: req.CommandId,
EdgeId: c.edgeInfo.ID,
Status: "error",
Error: err.Error(),
}, nil
}
resp.RequestId = req.RequestId
resp.CommandId = req.CommandId
resp.EdgeId = c.edgeInfo.ID
return resp, nil
}
// cpParserMap returns the ParserMap for Control Plane messages. It parses the
// hello response and the Control Plane-initiated status request, plus the
// status response the Edge marshals back.
@ -386,5 +437,17 @@ func cpParserMap() toki.ParserMap {
m := &iop.EdgeStatusResponse{}
return m, proto.Unmarshal(b, m)
},
toki.TypeNameOf(&iop.EdgeCommandRequest{}): func(b []byte) (proto.Message, error) {
m := &iop.EdgeCommandRequest{}
return m, proto.Unmarshal(b, m)
},
toki.TypeNameOf(&iop.EdgeCommandResponse{}): func(b []byte) (proto.Message, error) {
m := &iop.EdgeCommandResponse{}
return m, proto.Unmarshal(b, m)
},
toki.TypeNameOf(&iop.EdgeCommandEvent{}): func(b []byte) (proto.Message, error) {
m := &iop.EdgeCommandEvent{}
return m, proto.Unmarshal(b, m)
},
}
}

View file

@ -17,13 +17,42 @@ import (
)
type fakeStatusProvider struct {
nodes []edgeservice.NodeSnapshot
nodes []edgeservice.NodeSnapshot
capabilities []*iop.EdgeCapabilitySummary
domainAgents []*iop.EdgeDomainAgentSummary
executeErr error
executeResp *iop.EdgeCommandResponse
eventsToEmit []*iop.EdgeCommandEvent
}
func (f fakeStatusProvider) ListNodeSnapshots() []edgeservice.NodeSnapshot {
return append([]edgeservice.NodeSnapshot(nil), f.nodes...)
}
func (f fakeStatusProvider) GetCapabilities() []*iop.EdgeCapabilitySummary {
return f.capabilities
}
func (f fakeStatusProvider) GetDomainAgents() []*iop.EdgeDomainAgentSummary {
return f.domainAgents
}
func (f fakeStatusProvider) ExecuteCommand(ctx context.Context, req *iop.EdgeCommandRequest, onEvent func(*iop.EdgeCommandEvent)) (*iop.EdgeCommandResponse, error) {
for _, ev := range f.eventsToEmit {
onEvent(ev)
}
if f.executeErr != nil {
return nil, f.executeErr
}
if f.executeResp != nil {
return f.executeResp, nil
}
return &iop.EdgeCommandResponse{
Status: "completed",
Summary: "mock completed",
}, nil
}
// fakeCPParserMap returns a parser for EdgeHelloRequest on the server side.
func fakeCPParserMap() toki.ParserMap {
return toki.ParserMap{
@ -699,7 +728,7 @@ func TestConnectorStopPreventsReconnect(t *testing.T) {
}
}
func TestConnectorBuildStatusResponseWithConfig(t *testing.T) {
func TestConnectorBuildStatusResponseNonLeak(t *testing.T) {
fakeProvider := fakeStatusProvider{
nodes: []edgeservice.NodeSnapshot{
{
@ -708,7 +737,19 @@ func TestConnectorBuildStatusResponseWithConfig(t *testing.T) {
Label: "node0",
Config: &iop.NodeConfigPayload{
Runtime: &iop.NodeRuntimeConfig{
Concurrency: 5,
Concurrency: 5,
WorkspaceRoot: "/sensitive/workspace",
},
Adapters: []*iop.AdapterConfig{
{
Type: "ollama",
Enabled: true,
Config: &iop.AdapterConfig_Ollama{
Ollama: &iop.OllamaAdapterConfig{
BaseUrl: "http://sensitive-url:11434",
},
},
},
},
},
},
@ -719,7 +760,7 @@ func TestConnectorBuildStatusResponseWithConfig(t *testing.T) {
config.EdgeInfo{ID: "edge-1", Name: "Edge 1"},
config.EdgeControlPlaneConf{},
"1.0.0",
nil,
noopLogger(),
WithStatusProvider(fakeProvider),
)
@ -737,7 +778,122 @@ func TestConnectorBuildStatusResponseWithConfig(t *testing.T) {
t.Errorf("unexpected node: %+v", n)
}
if n.GetConfig() == nil || n.GetConfig().GetRuntime().GetConcurrency() != 5 {
t.Errorf("config not propagated properly: %+v", n.GetConfig())
if n.GetConfig() != nil {
t.Errorf("expected config to be nil (non-leak), but got: %+v", n.GetConfig())
}
}
func commandCapableCPParserMap() toki.ParserMap {
m := cpParserMap()
m[toki.TypeNameOf(&iop.EdgeHelloRequest{})] = func(b []byte) (proto.Message, error) {
req := &iop.EdgeHelloRequest{}
return req, proto.Unmarshal(b, req)
}
return m
}
func TestConnectorEdgeCommandRelay(t *testing.T) {
fakeProvider := fakeStatusProvider{
executeResp: &iop.EdgeCommandResponse{
Status: "completed",
Summary: "test OK",
},
eventsToEmit: []*iop.EdgeCommandEvent{
{CommandId: "cmd-1", Phase: "started", Summary: "started test"},
{CommandId: "cmd-1", Phase: "completed", Summary: "finished test"},
},
}
port, err := freeTCPPort()
if err != nil {
t.Fatalf("freeTCPPort: %v", err)
}
eventsReceived := make(chan *iop.EdgeCommandEvent, 8)
var cpClient *toki.TcpClient
srv := toki.NewTcpServer("127.0.0.1", port, func(conn net.Conn) *toki.TcpClient {
return toki.NewTcpClient(conn, 0, 0, commandCapableCPParserMap())
})
srv.OnClientConnected = func(client *toki.TcpClient) {
cpClient = client
toki.AddRequestListenerTyped[*iop.EdgeHelloRequest, *iop.EdgeHelloResponse](
&client.Communicator,
func(_ *iop.EdgeHelloRequest) (*iop.EdgeHelloResponse, error) {
return &iop.EdgeHelloResponse{Accepted: true, Protocol: "iop/1"}, nil
},
)
toki.AddListenerTyped[*iop.EdgeCommandEvent](&client.Communicator, func(ev *iop.EdgeCommandEvent) {
eventsReceived <- ev
})
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
if err := srv.Start(ctx); err != nil {
t.Fatalf("Start cp server: %v", err)
}
defer func() { _ = srv.Stop() }()
c := NewConnector(
config.EdgeInfo{ID: "edge-test-1", Name: "Edge Test"},
config.EdgeControlPlaneConf{
Enabled: true,
WireAddr: wireAddr(port),
ReconnectIntervalSec: 60,
},
"1.0.0",
noopLogger(),
WithStatusProvider(fakeProvider),
)
if err := c.Start(context.Background()); err != nil {
t.Fatalf("Start connector: %v", err)
}
defer c.Stop()
deadline := time.Now().Add(3 * time.Second)
for c.CurrentState() != StateConnected {
if time.Now().After(deadline) {
t.Fatalf("connector failed to connect, got %v", c.CurrentState())
}
time.Sleep(10 * time.Millisecond)
}
if cpClient == nil {
t.Fatal("control plane client not connected")
}
resp, err := toki.SendRequestTyped[*iop.EdgeCommandRequest, *iop.EdgeCommandResponse](
&cpClient.Communicator,
&iop.EdgeCommandRequest{
RequestId: "req-command-1",
CommandId: "cmd-1",
Operation: "test.op",
},
3*time.Second,
)
if err != nil {
t.Fatalf("SendRequestTyped edge command: %v", err)
}
if resp.GetStatus() != "completed" || resp.GetSummary() != "test OK" {
t.Errorf("unexpected response: %+v", resp)
}
if resp.GetEdgeId() != "edge-test-1" {
t.Errorf("expected edge_id edge-test-1, got %q", resp.GetEdgeId())
}
for i := 0; i < 2; i++ {
select {
case ev := <-eventsReceived:
if ev.GetCommandId() != "cmd-1" {
t.Errorf("unexpected event command_id: %q", ev.GetCommandId())
}
if ev.GetEdgeId() != "edge-test-1" {
t.Errorf("unexpected event edge_id: %q", ev.GetEdgeId())
}
case <-time.After(2 * time.Second):
t.Fatalf("timeout waiting for event %d to be relayed", i)
}
}
}

View file

@ -4,6 +4,7 @@ import (
"context"
"fmt"
"strconv"
"strings"
"time"
toki "git.toki-labs.com/toki/proto-socket/go"
@ -11,6 +12,7 @@ import (
edgeevents "iop/apps/edge/internal/events"
edgenode "iop/apps/edge/internal/node"
"iop/packages/go/config"
eventpkg "iop/packages/go/events"
iop "iop/proto/gen/iop"
)
@ -545,3 +547,262 @@ func normalizeTimeoutSec(timeoutSec int) int {
func nodeLabel(entry *edgenode.NodeEntry) string {
return entry.DisplayLabel()
}
func (s *Service) GetCapabilities() []*iop.EdgeCapabilitySummary {
snaps := s.ListNodeSnapshots()
hasGeneric := false
hasOTO := false
otoAvailable := false
var otoStates []string
for _, snap := range snaps {
if snap.AgentKind == config.AgentKindOTOAgent {
hasOTO = true
status, avail := lifecycleToSummaryStatus(snap.LifecycleState)
if avail {
otoAvailable = true
}
otoStates = append(otoStates, status)
} else {
hasGeneric = true
}
}
var out []*iop.EdgeCapabilitySummary
if hasGeneric {
out = append(out, &iop.EdgeCapabilitySummary{
Kind: "run",
Available: true,
Status: "ready",
Summary: "Generic execution node available",
})
}
if hasOTO {
otoStatus := "ready"
hasBusy := false
hasError := false
for _, st := range otoStates {
if st == "busy" {
hasBusy = true
} else if st == "error" {
hasError = true
}
}
if hasBusy {
otoStatus = "busy"
} else if hasError {
otoStatus = "error"
}
out = append(out, &iop.EdgeCapabilitySummary{
Kind: "build-deploy",
Available: otoAvailable,
Status: otoStatus,
Summary: "OTO build-deploy capability available",
})
}
return out
}
func (s *Service) GetDomainAgents() []*iop.EdgeDomainAgentSummary {
snaps := s.ListNodeSnapshots()
var out []*iop.EdgeDomainAgentSummary
for _, snap := range snaps {
if snap.AgentKind == config.AgentKindOTOAgent {
_, avail := lifecycleToSummaryStatus(snap.LifecycleState)
out = append(out, &iop.EdgeDomainAgentSummary{
AgentKind: snap.AgentKind,
Available: avail,
LifecycleState: snap.LifecycleState,
Summary: fmt.Sprintf("OTO agent alias=%s state=%s", snap.Alias, snap.LifecycleState),
})
}
}
return out
}
func lifecycleToSummaryStatus(state string) (string, bool) {
switch state {
case "", "connected":
return "ready", true
case "deploying", "running", "busy":
return "busy", true
case "error", "failed":
return "error", false
default:
return "busy", true
}
}
func edgeCommandResponseFromNodeCommand(req *iop.EdgeCommandRequest, view NodeCommandView, err error) *iop.EdgeCommandResponse {
if err != nil {
return &iop.EdgeCommandResponse{
Status: "error",
Error: err.Error(),
}
}
var summaries []string
var keys []string
for k := range view.Result {
keys = append(keys, k)
}
importSort := func(a, b string) bool { return a < b }
for i := 0; i < len(keys); i++ {
for j := i + 1; j < len(keys); j++ {
if importSort(keys[j], keys[i]) {
keys[i], keys[j] = keys[j], keys[i]
}
}
}
for _, k := range keys {
summaries = append(summaries, fmt.Sprintf("%s=%s", k, view.Result[k]))
}
summaryStr := fmt.Sprintf("NodeCommand %s completed: %s", req.GetParameters()["command"], strings.Join(summaries, ", "))
return &iop.EdgeCommandResponse{
Status: "completed",
Summary: summaryStr,
}
}
func unsupportedEdgeCommand(req *iop.EdgeCommandRequest, msg string) *iop.EdgeCommandResponse {
return &iop.EdgeCommandResponse{
Status: "unsupported",
Error: msg,
}
}
func (s *Service) ExecuteCommand(ctx context.Context, req *iop.EdgeCommandRequest, onEvent func(*iop.EdgeCommandEvent)) (*iop.EdgeCommandResponse, error) {
switch req.Operation {
case "health.check":
onEvent(&iop.EdgeCommandEvent{
CommandId: req.CommandId,
Phase: "started",
Summary: "Starting health check",
OccurredAt: time.Now().UnixNano(),
})
time.Sleep(10 * time.Millisecond) // simulation
onEvent(&iop.EdgeCommandEvent{
CommandId: req.CommandId,
Phase: "completed",
Summary: "Health check passed",
OccurredAt: time.Now().UnixNano(),
})
return &iop.EdgeCommandResponse{
Status: "completed",
Summary: "Edge health check OK",
}, nil
case "agent.status":
if req.TargetSelector == "" {
return nil, fmt.Errorf("target_selector is required for agent.status")
}
onEvent(&iop.EdgeCommandEvent{
CommandId: req.CommandId,
Phase: "started",
Summary: fmt.Sprintf("Resolving agent status for %s", req.TargetSelector),
OccurredAt: time.Now().UnixNano(),
})
entry, err := s.ResolveNode(req.TargetSelector)
if err != nil {
return &iop.EdgeCommandResponse{
Status: "error",
Error: err.Error(),
}, nil
}
snap := NodeEntrySnapshot(entry)
onEvent(&iop.EdgeCommandEvent{
CommandId: req.CommandId,
Phase: "completed",
Summary: fmt.Sprintf("Resolved agent status for %s", req.TargetSelector),
OccurredAt: time.Now().UnixNano(),
})
return &iop.EdgeCommandResponse{
Status: "completed",
Summary: fmt.Sprintf("node ID=%s Alias=%s Kind=%s State=%s", snap.NodeID, snap.Alias, snap.AgentKind, snap.LifecycleState),
}, nil
case "agent.command":
cmdName := req.GetParameters()["command"]
if cmdName == "" {
return unsupportedEdgeCommand(req, "missing command parameter for agent.command"), nil
}
onEvent(&iop.EdgeCommandEvent{
CommandId: req.CommandId,
Phase: "started",
Summary: fmt.Sprintf("Routing command %s to node %s", cmdName, req.TargetSelector),
OccurredAt: time.Now().UnixNano(),
})
entry, err := s.ResolveNode(req.TargetSelector)
if err != nil {
onEvent(&iop.EdgeCommandEvent{
CommandId: req.CommandId,
Phase: "failed",
Summary: fmt.Sprintf("Command %s failed: %v", cmdName, err),
OccurredAt: time.Now().UnixNano(),
})
return &iop.EdgeCommandResponse{
Status: "error",
Error: err.Error(),
}, nil
}
if entry.Client == nil {
onEvent(&iop.EdgeCommandEvent{
CommandId: req.CommandId,
Phase: "failed",
Summary: fmt.Sprintf("Command %s failed: node client not connected", cmdName),
OccurredAt: time.Now().UnixNano(),
})
return &iop.EdgeCommandResponse{
Status: "error",
Error: "node client not connected",
}, nil
}
switch cmdName {
case "capabilities":
view, err := s.Capabilities(ctx, NodeCommandRequestSpec{
NodeRef: req.TargetSelector,
Adapter: req.GetParameters()["adapter"],
Target: req.GetParameters()["target"],
SessionID: req.GetParameters()["session_id"],
TimeoutSec: 10,
})
var phase string
var summary string
if err != nil {
phase = "failed"
summary = fmt.Sprintf("Command %s failed: %v", cmdName, err)
} else {
phase = "completed"
summary = fmt.Sprintf("Command %s completed successfully", cmdName)
}
onEvent(&iop.EdgeCommandEvent{
CommandId: req.CommandId,
Phase: phase,
Summary: summary,
OccurredAt: time.Now().UnixNano(),
})
return edgeCommandResponseFromNodeCommand(req, view, err), nil
default:
return unsupportedEdgeCommand(req, fmt.Sprintf("unsupported agent.command command %q", cmdName)), nil
}
default:
return &iop.EdgeCommandResponse{
Status: "unsupported",
Error: fmt.Sprintf("unsupported operation %s", req.Operation),
}, nil
}
}

View file

@ -1,9 +1,16 @@
package service_test
import (
"context"
"fmt"
"net"
"strings"
"testing"
"time"
toki "git.toki-labs.com/toki/proto-socket/go"
"google.golang.org/protobuf/proto"
edgenode "iop/apps/edge/internal/node"
edgeservice "iop/apps/edge/internal/service"
"iop/packages/go/config"
@ -383,3 +390,356 @@ func TestResolveNodeSnapshotCarriesAgentKind(t *testing.T) {
t.Errorf("LifecycleState: got %q want %q", snap.LifecycleState, edgenode.LifecycleConnected)
}
}
func TestServiceCapabilitiesAndDomainAgents(t *testing.T) {
cases := []struct {
state string
expectedState string
expectedAvail bool
expectedStat string
}{
{"", "connected", true, "ready"},
{"connected", "connected", true, "ready"},
{"deploying", "deploying", true, "busy"},
{"running", "running", true, "busy"},
{"busy", "busy", true, "busy"},
{"error", "error", false, "error"},
{"failed", "failed", false, "error"},
{"unknown-state", "unknown-state", true, "busy"},
}
for _, tc := range cases {
t.Run(fmt.Sprintf("state-%s", tc.state), func(t *testing.T) {
reg := edgenode.NewRegistry()
reg.Register(&edgenode.NodeEntry{
NodeID: "generic-1",
Alias: "gen-node",
AgentKind: config.AgentKindGenericNode,
})
reg.Register(&edgenode.NodeEntry{
NodeID: "oto-1",
Alias: "deployer",
AgentKind: config.AgentKindOTOAgent,
LifecycleState: tc.state,
})
svc := edgeservice.New(reg, nil)
caps := svc.GetCapabilities()
if len(caps) != 2 {
t.Fatalf("expected 2 capabilities, got %d", len(caps))
}
capMap := map[string]*iop.EdgeCapabilitySummary{}
for _, c := range caps {
capMap[c.Kind] = c
}
if c := capMap["run"]; c == nil || !c.Available || c.Status != "ready" {
t.Errorf("invalid run capability: %+v", c)
}
if c := capMap["build-deploy"]; c == nil || c.Available != tc.expectedAvail || c.Status != tc.expectedStat {
t.Errorf("state %q: invalid build-deploy capability: %+v (want avail=%v, stat=%q)", tc.state, c, tc.expectedAvail, tc.expectedStat)
}
agents := svc.GetDomainAgents()
if len(agents) != 1 {
t.Fatalf("expected 1 domain agent, got %d", len(agents))
}
a := agents[0]
if a.AgentKind != config.AgentKindOTOAgent || a.Available != tc.expectedAvail || a.LifecycleState != tc.expectedState {
t.Errorf("invalid domain agent summary: %+v", a)
}
})
}
}
func TestServiceExecuteCommand(t *testing.T) {
reg := edgenode.NewRegistry()
reg.Register(&edgenode.NodeEntry{
NodeID: "oto-1",
Alias: "deployer",
AgentKind: config.AgentKindOTOAgent,
})
svc := edgeservice.New(reg, nil)
t.Run("health.check", func(t *testing.T) {
var events []*iop.EdgeCommandEvent
onEvent := func(e *iop.EdgeCommandEvent) {
events = append(events, e)
}
req := &iop.EdgeCommandRequest{
CommandId: "cmd-hc",
Operation: "health.check",
}
resp, err := svc.ExecuteCommand(context.Background(), req, onEvent)
if err != nil {
t.Fatalf("ExecuteCommand error: %v", err)
}
if resp.Status != "completed" {
t.Errorf("status: got %q want completed", resp.Status)
}
if len(events) != 2 {
t.Fatalf("expected 2 events, got %d", len(events))
}
if events[0].Phase != "started" || events[1].Phase != "completed" {
t.Errorf("unexpected event sequence: %+v", events)
}
})
t.Run("agent.status found", func(t *testing.T) {
var events []*iop.EdgeCommandEvent
onEvent := func(e *iop.EdgeCommandEvent) {
events = append(events, e)
}
req := &iop.EdgeCommandRequest{
CommandId: "cmd-status",
Operation: "agent.status",
TargetSelector: "deployer",
}
resp, err := svc.ExecuteCommand(context.Background(), req, onEvent)
if err != nil {
t.Fatalf("ExecuteCommand error: %v", err)
}
if resp.Status != "completed" {
t.Errorf("status: got %q want completed", resp.Status)
}
if len(events) != 2 {
t.Fatalf("expected 2 events, got %d", len(events))
}
if events[0].Phase != "started" || events[1].Phase != "completed" {
t.Errorf("unexpected event sequence: %+v", events)
}
})
t.Run("agent.status missing", func(t *testing.T) {
req := &iop.EdgeCommandRequest{
CommandId: "cmd-status",
Operation: "agent.status",
TargetSelector: "missing-agent",
}
resp, err := svc.ExecuteCommand(context.Background(), req, func(*iop.EdgeCommandEvent) {})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.Status != "error" || resp.Error == "" {
t.Errorf("expected error status, got: %+v", resp)
}
})
t.Run("agent.command does not fake success on nil client", func(t *testing.T) {
var events []*iop.EdgeCommandEvent
onEvent := func(e *iop.EdgeCommandEvent) {
events = append(events, e)
}
req := &iop.EdgeCommandRequest{
CommandId: "cmd-run",
Operation: "agent.command",
TargetSelector: "deployer",
Parameters: map[string]string{
"command": "capabilities",
},
}
resp, err := svc.ExecuteCommand(context.Background(), req, onEvent)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.Status != "error" {
t.Errorf("expected error status, got: %q", resp.Status)
}
if len(events) < 2 {
t.Fatalf("expected at least started and failed events, got %d", len(events))
}
if events[0].Phase != "started" || events[len(events)-1].Phase != "failed" {
t.Errorf("unexpected event sequence: %+v", events)
}
})
t.Run("agent.command missing command param", func(t *testing.T) {
req := &iop.EdgeCommandRequest{
CommandId: "cmd-run",
Operation: "agent.command",
TargetSelector: "deployer",
}
resp, err := svc.ExecuteCommand(context.Background(), req, func(*iop.EdgeCommandEvent) {})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.Status != "unsupported" {
t.Errorf("expected unsupported status, got %q", resp.Status)
}
})
t.Run("unsupported operation", func(t *testing.T) {
req := &iop.EdgeCommandRequest{
CommandId: "cmd-bad",
Operation: "bad.op",
}
resp, err := svc.ExecuteCommand(context.Background(), req, func(*iop.EdgeCommandEvent) {})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.Status != "unsupported" {
t.Errorf("status: got %q want unsupported", resp.Status)
}
})
}
func TestServiceExecuteCommandAgentCommandCapabilitiesSuccess(t *testing.T) {
edgeConn, nodeConn := net.Pipe()
defer edgeConn.Close()
defer nodeConn.Close()
parserMap := toki.ParserMap{
toki.TypeNameOf(&iop.NodeCommandRequest{}): func(b []byte) (proto.Message, error) {
m := &iop.NodeCommandRequest{}
return m, proto.Unmarshal(b, m)
},
toki.TypeNameOf(&iop.NodeCommandResponse{}): func(b []byte) (proto.Message, error) {
m := &iop.NodeCommandResponse{}
return m, proto.Unmarshal(b, m)
},
}
edgeClient := toki.NewTcpClient(edgeConn, 0, 0, parserMap)
nodeClient := toki.NewTcpClient(nodeConn, 0, 0, parserMap)
toki.AddRequestListenerTyped(&nodeClient.Communicator, func(req *iop.NodeCommandRequest) (*iop.NodeCommandResponse, error) {
if req.GetType() != iop.NodeCommandType_NODE_COMMAND_TYPE_CAPABILITIES {
return nil, fmt.Errorf("unexpected command type: %v", req.GetType())
}
if req.GetAdapter() != "cli" || req.GetTarget() != "codex" || req.GetSessionId() != "sess-success" {
return nil, fmt.Errorf("unexpected adapter/target/session: %s/%s/%s", req.GetAdapter(), req.GetTarget(), req.GetSessionId())
}
return &iop.NodeCommandResponse{
RequestId: req.RequestId,
Type: req.Type,
Result: map[string]string{"caps": "cli,codex"},
}, nil
})
reg := edgenode.NewRegistry()
reg.Register(&edgenode.NodeEntry{
NodeID: "oto-1",
Alias: "deployer",
AgentKind: config.AgentKindOTOAgent,
Client: edgeClient,
})
svc := edgeservice.New(reg, nil)
var events []*iop.EdgeCommandEvent
onEvent := func(e *iop.EdgeCommandEvent) {
events = append(events, e)
}
req := &iop.EdgeCommandRequest{
CommandId: "cmd-success-1",
Operation: "agent.command",
TargetSelector: "deployer",
Parameters: map[string]string{
"command": "capabilities",
"adapter": "cli",
"target": "codex",
"session_id": "sess-success",
},
}
resp, err := svc.ExecuteCommand(context.Background(), req, onEvent)
if err != nil {
t.Fatalf("ExecuteCommand: %v", err)
}
if resp.Status != "completed" {
t.Errorf("status: got %q want completed", resp.Status)
}
if !strings.Contains(resp.Summary, "caps=cli,codex") {
t.Errorf("summary: got %q, expected containing caps=cli,codex", resp.Summary)
}
if len(events) < 2 {
t.Fatalf("expected at least 2 events, got %d", len(events))
}
if events[0].Phase != "started" || events[len(events)-1].Phase != "completed" {
t.Errorf("unexpected event sequence: %+v", events)
}
if events[len(events)-1].Summary != "Command capabilities completed successfully" {
t.Errorf("unexpected event summary: got %q, expected 'Command capabilities completed successfully'", events[len(events)-1].Summary)
}
}
func TestServiceExecuteCommandAgentCommandCapabilitiesNodeError(t *testing.T) {
edgeConn, nodeConn := net.Pipe()
defer edgeConn.Close()
defer nodeConn.Close()
parserMap := toki.ParserMap{
toki.TypeNameOf(&iop.NodeCommandRequest{}): func(b []byte) (proto.Message, error) {
m := &iop.NodeCommandRequest{}
return m, proto.Unmarshal(b, m)
},
toki.TypeNameOf(&iop.NodeCommandResponse{}): func(b []byte) (proto.Message, error) {
m := &iop.NodeCommandResponse{}
return m, proto.Unmarshal(b, m)
},
}
edgeClient := toki.NewTcpClient(edgeConn, 0, 0, parserMap)
nodeClient := toki.NewTcpClient(nodeConn, 0, 0, parserMap)
toki.AddRequestListenerTyped(&nodeClient.Communicator, func(req *iop.NodeCommandRequest) (*iop.NodeCommandResponse, error) {
if req.GetSessionId() != "sess-error" {
return nil, fmt.Errorf("unexpected session_id: %s", req.GetSessionId())
}
return &iop.NodeCommandResponse{
RequestId: req.RequestId,
Type: req.Type,
Error: "node failure",
}, nil
})
reg := edgenode.NewRegistry()
reg.Register(&edgenode.NodeEntry{
NodeID: "oto-1",
Alias: "deployer",
AgentKind: config.AgentKindOTOAgent,
Client: edgeClient,
})
svc := edgeservice.New(reg, nil)
var events []*iop.EdgeCommandEvent
onEvent := func(e *iop.EdgeCommandEvent) {
events = append(events, e)
}
req := &iop.EdgeCommandRequest{
CommandId: "cmd-error-1",
Operation: "agent.command",
TargetSelector: "deployer",
Parameters: map[string]string{
"command": "capabilities",
"adapter": "cli",
"target": "codex",
"session_id": "sess-error",
},
}
resp, err := svc.ExecuteCommand(context.Background(), req, onEvent)
if err != nil {
t.Fatalf("ExecuteCommand: %v", err)
}
if resp.Status != "error" {
t.Errorf("status: got %q want error", resp.Status)
}
if !strings.Contains(resp.Error, "node reported error: node failure") {
t.Errorf("error: got %q, expected containing node failure", resp.Error)
}
if len(events) < 2 {
t.Fatalf("expected at least 2 events, got %d", len(events))
}
if events[0].Phase != "started" || events[len(events)-1].Phase != "failed" {
t.Errorf("unexpected event sequence: %+v", events)
}
expectedSummary := "Command capabilities failed: node reported error: node failure"
if events[len(events)-1].Summary != expectedSummary {
t.Errorf("unexpected event summary: got %q, expected %q", events[len(events)-1].Summary, expectedSummary)
}
}

View file

@ -622,14 +622,16 @@ func (x *EdgeNodeSnapshot) GetConfig() *NodeConfigPayload {
// EdgeStatusRequest. The Control Plane observes this snapshot rather than
// replicating the Edge-local registry.
type EdgeStatusResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"`
EdgeId string `protobuf:"bytes,2,opt,name=edge_id,json=edgeId,proto3" json:"edge_id,omitempty"`
EdgeName string `protobuf:"bytes,3,opt,name=edge_name,json=edgeName,proto3" json:"edge_name,omitempty"`
ObservedTimeUnixNano int64 `protobuf:"varint,4,opt,name=observed_time_unix_nano,json=observedTimeUnixNano,proto3" json:"observed_time_unix_nano,omitempty"`
Nodes []*EdgeNodeSnapshot `protobuf:"bytes,5,rep,name=nodes,proto3" json:"nodes,omitempty"`
Metadata map[string]string `protobuf:"bytes,6,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
Error string `protobuf:"bytes,7,opt,name=error,proto3" json:"error,omitempty"`
state protoimpl.MessageState `protogen:"open.v1"`
RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"`
EdgeId string `protobuf:"bytes,2,opt,name=edge_id,json=edgeId,proto3" json:"edge_id,omitempty"`
EdgeName string `protobuf:"bytes,3,opt,name=edge_name,json=edgeName,proto3" json:"edge_name,omitempty"`
ObservedTimeUnixNano int64 `protobuf:"varint,4,opt,name=observed_time_unix_nano,json=observedTimeUnixNano,proto3" json:"observed_time_unix_nano,omitempty"`
Nodes []*EdgeNodeSnapshot `protobuf:"bytes,5,rep,name=nodes,proto3" json:"nodes,omitempty"`
Metadata map[string]string `protobuf:"bytes,6,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
Error string `protobuf:"bytes,7,opt,name=error,proto3" json:"error,omitempty"`
Capabilities []*EdgeCapabilitySummary `protobuf:"bytes,8,rep,name=capabilities,proto3" json:"capabilities,omitempty"`
DomainAgents []*EdgeDomainAgentSummary `protobuf:"bytes,9,rep,name=domain_agents,json=domainAgents,proto3" json:"domain_agents,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@ -713,6 +715,400 @@ func (x *EdgeStatusResponse) GetError() string {
return ""
}
func (x *EdgeStatusResponse) GetCapabilities() []*EdgeCapabilitySummary {
if x != nil {
return x.Capabilities
}
return nil
}
func (x *EdgeStatusResponse) GetDomainAgents() []*EdgeDomainAgentSummary {
if x != nil {
return x.DomainAgents
}
return nil
}
type EdgeCapabilitySummary struct {
state protoimpl.MessageState `protogen:"open.v1"`
Kind string `protobuf:"bytes,1,opt,name=kind,proto3" json:"kind,omitempty"`
Available bool `protobuf:"varint,2,opt,name=available,proto3" json:"available,omitempty"`
Status string `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"`
Summary string `protobuf:"bytes,4,opt,name=summary,proto3" json:"summary,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *EdgeCapabilitySummary) Reset() {
*x = EdgeCapabilitySummary{}
mi := &file_proto_iop_control_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *EdgeCapabilitySummary) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*EdgeCapabilitySummary) ProtoMessage() {}
func (x *EdgeCapabilitySummary) ProtoReflect() protoreflect.Message {
mi := &file_proto_iop_control_proto_msgTypes[10]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use EdgeCapabilitySummary.ProtoReflect.Descriptor instead.
func (*EdgeCapabilitySummary) Descriptor() ([]byte, []int) {
return file_proto_iop_control_proto_rawDescGZIP(), []int{10}
}
func (x *EdgeCapabilitySummary) GetKind() string {
if x != nil {
return x.Kind
}
return ""
}
func (x *EdgeCapabilitySummary) GetAvailable() bool {
if x != nil {
return x.Available
}
return false
}
func (x *EdgeCapabilitySummary) GetStatus() string {
if x != nil {
return x.Status
}
return ""
}
func (x *EdgeCapabilitySummary) GetSummary() string {
if x != nil {
return x.Summary
}
return ""
}
type EdgeDomainAgentSummary struct {
state protoimpl.MessageState `protogen:"open.v1"`
AgentKind string `protobuf:"bytes,1,opt,name=agent_kind,json=agentKind,proto3" json:"agent_kind,omitempty"`
Available bool `protobuf:"varint,2,opt,name=available,proto3" json:"available,omitempty"`
LifecycleState string `protobuf:"bytes,3,opt,name=lifecycle_state,json=lifecycleState,proto3" json:"lifecycle_state,omitempty"`
ActiveCommandId string `protobuf:"bytes,4,opt,name=active_command_id,json=activeCommandId,proto3" json:"active_command_id,omitempty"`
Summary string `protobuf:"bytes,5,opt,name=summary,proto3" json:"summary,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *EdgeDomainAgentSummary) Reset() {
*x = EdgeDomainAgentSummary{}
mi := &file_proto_iop_control_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *EdgeDomainAgentSummary) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*EdgeDomainAgentSummary) ProtoMessage() {}
func (x *EdgeDomainAgentSummary) ProtoReflect() protoreflect.Message {
mi := &file_proto_iop_control_proto_msgTypes[11]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use EdgeDomainAgentSummary.ProtoReflect.Descriptor instead.
func (*EdgeDomainAgentSummary) Descriptor() ([]byte, []int) {
return file_proto_iop_control_proto_rawDescGZIP(), []int{11}
}
func (x *EdgeDomainAgentSummary) GetAgentKind() string {
if x != nil {
return x.AgentKind
}
return ""
}
func (x *EdgeDomainAgentSummary) GetAvailable() bool {
if x != nil {
return x.Available
}
return false
}
func (x *EdgeDomainAgentSummary) GetLifecycleState() string {
if x != nil {
return x.LifecycleState
}
return ""
}
func (x *EdgeDomainAgentSummary) GetActiveCommandId() string {
if x != nil {
return x.ActiveCommandId
}
return ""
}
func (x *EdgeDomainAgentSummary) GetSummary() string {
if x != nil {
return x.Summary
}
return ""
}
type EdgeCommandRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"`
CommandId string `protobuf:"bytes,2,opt,name=command_id,json=commandId,proto3" json:"command_id,omitempty"`
TargetSelector string `protobuf:"bytes,3,opt,name=target_selector,json=targetSelector,proto3" json:"target_selector,omitempty"`
Operation string `protobuf:"bytes,4,opt,name=operation,proto3" json:"operation,omitempty"`
Parameters map[string]string `protobuf:"bytes,5,rep,name=parameters,proto3" json:"parameters,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *EdgeCommandRequest) Reset() {
*x = EdgeCommandRequest{}
mi := &file_proto_iop_control_proto_msgTypes[12]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *EdgeCommandRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*EdgeCommandRequest) ProtoMessage() {}
func (x *EdgeCommandRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_iop_control_proto_msgTypes[12]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use EdgeCommandRequest.ProtoReflect.Descriptor instead.
func (*EdgeCommandRequest) Descriptor() ([]byte, []int) {
return file_proto_iop_control_proto_rawDescGZIP(), []int{12}
}
func (x *EdgeCommandRequest) GetRequestId() string {
if x != nil {
return x.RequestId
}
return ""
}
func (x *EdgeCommandRequest) GetCommandId() string {
if x != nil {
return x.CommandId
}
return ""
}
func (x *EdgeCommandRequest) GetTargetSelector() string {
if x != nil {
return x.TargetSelector
}
return ""
}
func (x *EdgeCommandRequest) GetOperation() string {
if x != nil {
return x.Operation
}
return ""
}
func (x *EdgeCommandRequest) GetParameters() map[string]string {
if x != nil {
return x.Parameters
}
return nil
}
type EdgeCommandResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"`
CommandId string `protobuf:"bytes,2,opt,name=command_id,json=commandId,proto3" json:"command_id,omitempty"`
EdgeId string `protobuf:"bytes,3,opt,name=edge_id,json=edgeId,proto3" json:"edge_id,omitempty"`
Status string `protobuf:"bytes,4,opt,name=status,proto3" json:"status,omitempty"`
Summary string `protobuf:"bytes,5,opt,name=summary,proto3" json:"summary,omitempty"`
Error string `protobuf:"bytes,6,opt,name=error,proto3" json:"error,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *EdgeCommandResponse) Reset() {
*x = EdgeCommandResponse{}
mi := &file_proto_iop_control_proto_msgTypes[13]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *EdgeCommandResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*EdgeCommandResponse) ProtoMessage() {}
func (x *EdgeCommandResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_iop_control_proto_msgTypes[13]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use EdgeCommandResponse.ProtoReflect.Descriptor instead.
func (*EdgeCommandResponse) Descriptor() ([]byte, []int) {
return file_proto_iop_control_proto_rawDescGZIP(), []int{13}
}
func (x *EdgeCommandResponse) GetRequestId() string {
if x != nil {
return x.RequestId
}
return ""
}
func (x *EdgeCommandResponse) GetCommandId() string {
if x != nil {
return x.CommandId
}
return ""
}
func (x *EdgeCommandResponse) GetEdgeId() string {
if x != nil {
return x.EdgeId
}
return ""
}
func (x *EdgeCommandResponse) GetStatus() string {
if x != nil {
return x.Status
}
return ""
}
func (x *EdgeCommandResponse) GetSummary() string {
if x != nil {
return x.Summary
}
return ""
}
func (x *EdgeCommandResponse) GetError() string {
if x != nil {
return x.Error
}
return ""
}
type EdgeCommandEvent struct {
state protoimpl.MessageState `protogen:"open.v1"`
CommandId string `protobuf:"bytes,1,opt,name=command_id,json=commandId,proto3" json:"command_id,omitempty"`
EdgeId string `protobuf:"bytes,2,opt,name=edge_id,json=edgeId,proto3" json:"edge_id,omitempty"`
Phase string `protobuf:"bytes,3,opt,name=phase,proto3" json:"phase,omitempty"`
Summary string `protobuf:"bytes,4,opt,name=summary,proto3" json:"summary,omitempty"`
OccurredAt int64 `protobuf:"varint,5,opt,name=occurred_at,json=occurredAt,proto3" json:"occurred_at,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *EdgeCommandEvent) Reset() {
*x = EdgeCommandEvent{}
mi := &file_proto_iop_control_proto_msgTypes[14]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *EdgeCommandEvent) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*EdgeCommandEvent) ProtoMessage() {}
func (x *EdgeCommandEvent) ProtoReflect() protoreflect.Message {
mi := &file_proto_iop_control_proto_msgTypes[14]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use EdgeCommandEvent.ProtoReflect.Descriptor instead.
func (*EdgeCommandEvent) Descriptor() ([]byte, []int) {
return file_proto_iop_control_proto_rawDescGZIP(), []int{14}
}
func (x *EdgeCommandEvent) GetCommandId() string {
if x != nil {
return x.CommandId
}
return ""
}
func (x *EdgeCommandEvent) GetEdgeId() string {
if x != nil {
return x.EdgeId
}
return ""
}
func (x *EdgeCommandEvent) GetPhase() string {
if x != nil {
return x.Phase
}
return ""
}
func (x *EdgeCommandEvent) GetSummary() string {
if x != nil {
return x.Summary
}
return ""
}
func (x *EdgeCommandEvent) GetOccurredAt() int64 {
if x != nil {
return x.OccurredAt
}
return 0
}
var File_proto_iop_control_proto protoreflect.FileDescriptor
const file_proto_iop_control_proto_rawDesc = "" +
@ -771,7 +1167,7 @@ const file_proto_iop_control_proto_rawDesc = "" +
"\x05alias\x18\x02 \x01(\tR\x05alias\x12\x14\n" +
"\x05label\x18\x03 \x01(\tR\x05label\x12\x1c\n" +
"\tconnected\x18\x04 \x01(\bR\tconnected\x12.\n" +
"\x06config\x18\x05 \x01(\v2\x16.iop.NodeConfigPayloadR\x06config\"\xe3\x02\n" +
"\x06config\x18\x05 \x01(\v2\x16.iop.NodeConfigPayloadR\x06config\"\xe5\x03\n" +
"\x12EdgeStatusResponse\x12\x1d\n" +
"\n" +
"request_id\x18\x01 \x01(\tR\trequestId\x12\x17\n" +
@ -780,10 +1176,54 @@ const file_proto_iop_control_proto_rawDesc = "" +
"\x17observed_time_unix_nano\x18\x04 \x01(\x03R\x14observedTimeUnixNano\x12+\n" +
"\x05nodes\x18\x05 \x03(\v2\x15.iop.EdgeNodeSnapshotR\x05nodes\x12A\n" +
"\bmetadata\x18\x06 \x03(\v2%.iop.EdgeStatusResponse.MetadataEntryR\bmetadata\x12\x14\n" +
"\x05error\x18\a \x01(\tR\x05error\x1a;\n" +
"\x05error\x18\a \x01(\tR\x05error\x12>\n" +
"\fcapabilities\x18\b \x03(\v2\x1a.iop.EdgeCapabilitySummaryR\fcapabilities\x12@\n" +
"\rdomain_agents\x18\t \x03(\v2\x1b.iop.EdgeDomainAgentSummaryR\fdomainAgents\x1a;\n" +
"\rMetadataEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x13Z\x11iop/proto/gen/iopb\x06proto3"
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"{\n" +
"\x15EdgeCapabilitySummary\x12\x12\n" +
"\x04kind\x18\x01 \x01(\tR\x04kind\x12\x1c\n" +
"\tavailable\x18\x02 \x01(\bR\tavailable\x12\x16\n" +
"\x06status\x18\x03 \x01(\tR\x06status\x12\x18\n" +
"\asummary\x18\x04 \x01(\tR\asummary\"\xc4\x01\n" +
"\x16EdgeDomainAgentSummary\x12\x1d\n" +
"\n" +
"agent_kind\x18\x01 \x01(\tR\tagentKind\x12\x1c\n" +
"\tavailable\x18\x02 \x01(\bR\tavailable\x12'\n" +
"\x0flifecycle_state\x18\x03 \x01(\tR\x0elifecycleState\x12*\n" +
"\x11active_command_id\x18\x04 \x01(\tR\x0factiveCommandId\x12\x18\n" +
"\asummary\x18\x05 \x01(\tR\asummary\"\xa1\x02\n" +
"\x12EdgeCommandRequest\x12\x1d\n" +
"\n" +
"request_id\x18\x01 \x01(\tR\trequestId\x12\x1d\n" +
"\n" +
"command_id\x18\x02 \x01(\tR\tcommandId\x12'\n" +
"\x0ftarget_selector\x18\x03 \x01(\tR\x0etargetSelector\x12\x1c\n" +
"\toperation\x18\x04 \x01(\tR\toperation\x12G\n" +
"\n" +
"parameters\x18\x05 \x03(\v2'.iop.EdgeCommandRequest.ParametersEntryR\n" +
"parameters\x1a=\n" +
"\x0fParametersEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xb4\x01\n" +
"\x13EdgeCommandResponse\x12\x1d\n" +
"\n" +
"request_id\x18\x01 \x01(\tR\trequestId\x12\x1d\n" +
"\n" +
"command_id\x18\x02 \x01(\tR\tcommandId\x12\x17\n" +
"\aedge_id\x18\x03 \x01(\tR\x06edgeId\x12\x16\n" +
"\x06status\x18\x04 \x01(\tR\x06status\x12\x18\n" +
"\asummary\x18\x05 \x01(\tR\asummary\x12\x14\n" +
"\x05error\x18\x06 \x01(\tR\x05error\"\x9b\x01\n" +
"\x10EdgeCommandEvent\x12\x1d\n" +
"\n" +
"command_id\x18\x01 \x01(\tR\tcommandId\x12\x17\n" +
"\aedge_id\x18\x02 \x01(\tR\x06edgeId\x12\x14\n" +
"\x05phase\x18\x03 \x01(\tR\x05phase\x12\x18\n" +
"\asummary\x18\x04 \x01(\tR\asummary\x12\x1f\n" +
"\voccurred_at\x18\x05 \x01(\x03R\n" +
"occurredAtB\x13Z\x11iop/proto/gen/iopb\x06proto3"
var (
file_proto_iop_control_proto_rawDescOnce sync.Once
@ -797,37 +1237,46 @@ func file_proto_iop_control_proto_rawDescGZIP() []byte {
return file_proto_iop_control_proto_rawDescData
}
var file_proto_iop_control_proto_msgTypes = make([]protoimpl.MessageInfo, 14)
var file_proto_iop_control_proto_msgTypes = make([]protoimpl.MessageInfo, 20)
var file_proto_iop_control_proto_goTypes = []any{
(*PolicyRule)(nil), // 0: iop.PolicyRule
(*ScheduleRequest)(nil), // 1: iop.ScheduleRequest
(*ScheduleResponse)(nil), // 2: iop.ScheduleResponse
(*ClientHelloRequest)(nil), // 3: iop.ClientHelloRequest
(*ClientHelloResponse)(nil), // 4: iop.ClientHelloResponse
(*EdgeHelloRequest)(nil), // 5: iop.EdgeHelloRequest
(*EdgeHelloResponse)(nil), // 6: iop.EdgeHelloResponse
(*EdgeStatusRequest)(nil), // 7: iop.EdgeStatusRequest
(*EdgeNodeSnapshot)(nil), // 8: iop.EdgeNodeSnapshot
(*EdgeStatusResponse)(nil), // 9: iop.EdgeStatusResponse
nil, // 10: iop.PolicyRule.ParamsEntry
nil, // 11: iop.ScheduleRequest.MetadataEntry
nil, // 12: iop.EdgeHelloRequest.MetadataEntry
nil, // 13: iop.EdgeStatusResponse.MetadataEntry
(*NodeConfigPayload)(nil), // 14: iop.NodeConfigPayload
(*PolicyRule)(nil), // 0: iop.PolicyRule
(*ScheduleRequest)(nil), // 1: iop.ScheduleRequest
(*ScheduleResponse)(nil), // 2: iop.ScheduleResponse
(*ClientHelloRequest)(nil), // 3: iop.ClientHelloRequest
(*ClientHelloResponse)(nil), // 4: iop.ClientHelloResponse
(*EdgeHelloRequest)(nil), // 5: iop.EdgeHelloRequest
(*EdgeHelloResponse)(nil), // 6: iop.EdgeHelloResponse
(*EdgeStatusRequest)(nil), // 7: iop.EdgeStatusRequest
(*EdgeNodeSnapshot)(nil), // 8: iop.EdgeNodeSnapshot
(*EdgeStatusResponse)(nil), // 9: iop.EdgeStatusResponse
(*EdgeCapabilitySummary)(nil), // 10: iop.EdgeCapabilitySummary
(*EdgeDomainAgentSummary)(nil), // 11: iop.EdgeDomainAgentSummary
(*EdgeCommandRequest)(nil), // 12: iop.EdgeCommandRequest
(*EdgeCommandResponse)(nil), // 13: iop.EdgeCommandResponse
(*EdgeCommandEvent)(nil), // 14: iop.EdgeCommandEvent
nil, // 15: iop.PolicyRule.ParamsEntry
nil, // 16: iop.ScheduleRequest.MetadataEntry
nil, // 17: iop.EdgeHelloRequest.MetadataEntry
nil, // 18: iop.EdgeStatusResponse.MetadataEntry
nil, // 19: iop.EdgeCommandRequest.ParametersEntry
(*NodeConfigPayload)(nil), // 20: iop.NodeConfigPayload
}
var file_proto_iop_control_proto_depIdxs = []int32{
10, // 0: iop.PolicyRule.params:type_name -> iop.PolicyRule.ParamsEntry
15, // 0: iop.PolicyRule.params:type_name -> iop.PolicyRule.ParamsEntry
0, // 1: iop.ScheduleRequest.policies:type_name -> iop.PolicyRule
11, // 2: iop.ScheduleRequest.metadata:type_name -> iop.ScheduleRequest.MetadataEntry
12, // 3: iop.EdgeHelloRequest.metadata:type_name -> iop.EdgeHelloRequest.MetadataEntry
14, // 4: iop.EdgeNodeSnapshot.config:type_name -> iop.NodeConfigPayload
16, // 2: iop.ScheduleRequest.metadata:type_name -> iop.ScheduleRequest.MetadataEntry
17, // 3: iop.EdgeHelloRequest.metadata:type_name -> iop.EdgeHelloRequest.MetadataEntry
20, // 4: iop.EdgeNodeSnapshot.config:type_name -> iop.NodeConfigPayload
8, // 5: iop.EdgeStatusResponse.nodes:type_name -> iop.EdgeNodeSnapshot
13, // 6: iop.EdgeStatusResponse.metadata:type_name -> iop.EdgeStatusResponse.MetadataEntry
7, // [7:7] is the sub-list for method output_type
7, // [7:7] is the sub-list for method input_type
7, // [7:7] is the sub-list for extension type_name
7, // [7:7] is the sub-list for extension extendee
0, // [0:7] is the sub-list for field type_name
18, // 6: iop.EdgeStatusResponse.metadata:type_name -> iop.EdgeStatusResponse.MetadataEntry
10, // 7: iop.EdgeStatusResponse.capabilities:type_name -> iop.EdgeCapabilitySummary
11, // 8: iop.EdgeStatusResponse.domain_agents:type_name -> iop.EdgeDomainAgentSummary
19, // 9: iop.EdgeCommandRequest.parameters:type_name -> iop.EdgeCommandRequest.ParametersEntry
10, // [10:10] is the sub-list for method output_type
10, // [10:10] is the sub-list for method input_type
10, // [10:10] is the sub-list for extension type_name
10, // [10:10] is the sub-list for extension extendee
0, // [0:10] is the sub-list for field type_name
}
func init() { file_proto_iop_control_proto_init() }
@ -842,7 +1291,7 @@ func file_proto_iop_control_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_iop_control_proto_rawDesc), len(file_proto_iop_control_proto_rawDesc)),
NumEnums: 0,
NumMessages: 14,
NumMessages: 20,
NumExtensions: 0,
NumServices: 0,
},

View file

@ -93,4 +93,46 @@ message EdgeStatusResponse {
repeated EdgeNodeSnapshot nodes = 5;
map<string, string> metadata = 6;
string error = 7;
repeated EdgeCapabilitySummary capabilities = 8;
repeated EdgeDomainAgentSummary domain_agents = 9;
}
message EdgeCapabilitySummary {
string kind = 1;
bool available = 2;
string status = 3;
string summary = 4;
}
message EdgeDomainAgentSummary {
string agent_kind = 1;
bool available = 2;
string lifecycle_state = 3;
string active_command_id = 4;
string summary = 5;
}
message EdgeCommandRequest {
string request_id = 1;
string command_id = 2;
string target_selector = 3;
string operation = 4;
map<string, string> parameters = 5;
}
message EdgeCommandResponse {
string request_id = 1;
string command_id = 2;
string edge_id = 3;
string status = 4;
string summary = 5;
string error = 6;
}
message EdgeCommandEvent {
string command_id = 1;
string edge_id = 2;
string phase = 3;
string summary = 4;
int64 occurred_at = 5;
}