<!-- task=m-multi-edge-operations/01_history_agent_contract plan=0 tag=API -->

# Plan - History Agent State Backend Contract

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

구현 완료는 active `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션까지 실제 구현 내용과 검증 출력으로 채운 뒤에만 성립한다. 검증 명령을 실행하고 실제 stdout/stderr를 review stub에 붙인다. active 파일은 그대로 두고 리뷰 준비 상태를 보고하며, `complete.log` 작성, `.log` rename, archive 이동은 code-review 스킬 전용이다. 구현 중 사용자 결정, 사용자 소유 외부 환경/secret/서비스 준비, 또는 계획 범위 충돌로 막히면 review stub의 `사용자 리뷰 요청`에 정확한 근거를 남기고 멈춘다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 증거 공백은 사용자 리뷰 요청이 아니다.

## 배경

`history-agent-state`는 Edge별 실행/명령 결과와 domain agent summary가 섞이지 않는 운영면을 요구한다. Control Plane에는 이미 `/fleet/status`, `/edges/{id}/operations`, command relay가 있지만, Edge별로 서로 다른 domain agent 상태와 command audit이 분리되는 HTTP 계약 검증이 약하다. 이 선행 작업은 client 작업이 의존할 backend JSON 계약을 작고 결정적으로 고정한다.

## 사용자 리뷰 요청 흐름

구현 중 blocker는 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 해당 섹션은 `agent-ops/skills/common/_templates/implementation-user-review-request-section.md` 기준이며, 실제 `USER_REVIEW.md` 작성 여부 판단은 code-review가 소유한다.

## 분석 결과

### 읽은 파일

- `agent-roadmap/current.md`
- `agent-roadmap/phase/control-plane-portal-ops/PHASE.md`
- `agent-roadmap/phase/control-plane-portal-ops/milestones/multi-edge-operations.md`
- `agent-test/local/rules.md`
- `agent-test/local/control-plane-smoke.md`
- `agent-test/local/edge-smoke.md`
- `agent-test/local/client-smoke.md`
- `agent-test/local/platform-common-smoke.md`
- `agent-ops/rules/project/domain/control-plane/rules.md`
- `agent-ops/rules/project/domain/edge/rules.md`
- `agent-ops/rules/project/domain/client/rules.md`
- `agent-ops/rules/project/domain/testing/rules.md`
- `apps/control-plane/cmd/control-plane/main.go`
- `apps/control-plane/cmd/control-plane/main_test.go`
- `apps/control-plane/internal/wire/edge_registry.go`
- `apps/control-plane/internal/wire/edge_server.go`
- `apps/edge/internal/service/service.go`
- `apps/edge/internal/service/service_test.go`
- `proto/iop/control.proto`

### 테스트 환경 규칙

- test_env: `local`.
- `agent-test/local/rules.md` present/read. Matching profiles read: `control-plane-smoke`, `edge-smoke`, `client-smoke`, `platform-common-smoke`.
- 적용 명령: control-plane 변경은 `go test ./apps/control-plane/...`; Edge wire/registry lifecycle가 바뀌면 `make test-control-plane-edge-wire`; edge service가 바뀌면 `go test ./apps/edge/...`.
- 이 subtask는 backend HTTP contract/test 고정이므로 primary verification은 focused Go tests다. Proto 원본 변경은 계획 범위 밖이며, proto를 바꾸면 별도 `make proto proto-dart`가 필요하다.
- `<확인 필요>` 값 없음.

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

- `/fleet/status`는 `TestFleetStatusHTTPHandlerCombinesConnectionAndCapabilities`에서 online/degraded/offline과 capability를 확인하지만, 두 connected Edge의 서로 다른 `domain_agents` 구분은 고정하지 않는다.
- `/edges/{id}/operations`는 `TestEdgeOperationsHTTPHandlerListsAudit`에서 단일 Edge audit만 확인한다. 다른 Edge command가 섞이지 않는지, 각 Edge 조회가 자기 command만 반환하는지 고정하지 않는다.
- Edge service는 `TestServiceCapabilitiesAndDomainAgents`와 `TestServiceExecuteCommand...`로 domain agent summary와 command event를 확인한다. Control Plane HTTP aggregation과 Edge별 isolation을 함께 검증하지는 않는다.

### 심볼 참조

- none. Rename/remove 없음.

### 분할 판단

- split decision policy 적용 완료.
- shared task group: `agent-task/m-multi-edge-operations`.
- `01_history_agent_contract`: backend HTTP/registry contract 검증. 선행 없음.
- `02+01_history_agent_client`: Flutter 운영 화면 구현/검증. `01_history_agent_contract`의 `complete.log`가 필요하다.
- split 이유: backend JSON contract와 Flutter UI state는 ownership, verification command, failure profile이 다르다.

### 범위 결정 근거

- 포함: `apps/control-plane/cmd/control-plane/main_test.go`; 테스트 실패가 실제 contract gap을 드러낼 때만 `apps/control-plane/cmd/control-plane/main.go`.
- 포함 가능: Edge domain agent summary 생성에 gap이 있으면 `apps/edge/internal/service/service.go`와 `apps/edge/internal/service/service_test.go`.
- 제외: proto schema 변경, durable audit/history 저장소, OTO artifact/log 원본 저장, Flutter UI 변경, docs/README 갱신.
- Control Plane은 Edge-owned summary만 관찰한다. Node token/address/config raw settings를 HTTP response에 추가하지 않는다.

### 빌드 등급

- build lane/grade: `local-G05`. Go HTTP contract와 focused tests로 검증 가능한 bounded backend 작업이다.

## 구현 체크리스트

- [ ] `/fleet/status`가 두 connected Edge의 서로 다른 domain agent capability/status/command summary를 구분해 반환하는 focused test를 추가한다.
- [ ] `/edges/{edge_id}/operations`가 Edge별 command audit을 섞지 않는 focused test를 추가한다.
- [ ] 필요 시 backend mapper/registry 구현을 테스트 기대에 맞게 보강하되 Control Plane이 Edge-owned 원본 데이터를 소유하지 않도록 유지한다.
- [ ] `go test ./apps/control-plane/cmd/control-plane ./apps/control-plane/internal/wire ./apps/edge/internal/service -count=1`를 실행한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [API-1] Fleet Status Domain Agent Isolation

문제: `apps/control-plane/cmd/control-plane/main.go:501`의 `/fleet/status`는 registry snapshot별로 `fleetEdgeViewFromState`를 호출하고, `main.go:690`에서 live status의 domain agents를 view에 복사한다. 기존 `apps/control-plane/cmd/control-plane/main_test.go:675`는 capability와 health만 고정하고 두 Edge의 서로 다른 `domain_agents` 구분을 검증하지 않는다.

해결 방법: `TestFleetStatusHTTPHandlerCombinesConnectionAndCapabilities`에 connected Edge를 하나 더 추가하거나 subtest를 추가한다. `requestStatus`는 Edge ID별로 서로 다른 `EdgeDomainAgentSummary`를 반환하게 한다. JSON decode 후 edge map에서 `edge-online`과 새 Edge의 `DomainAgents`가 각각 기대 `agent_kind`, `lifecycle_state`, `active_command_id`, `summary`를 갖는지 확인한다. 기존 node internals leak guard는 유지한다.

수정 파일 및 체크리스트:

- [ ] `apps/control-plane/cmd/control-plane/main_test.go`: fleet status test fixture에 distinct domain agent summary 추가.
- [ ] `apps/control-plane/cmd/control-plane/main_test.go`: Edge별 `DomainAgents` assertion 추가.
- [ ] `apps/control-plane/cmd/control-plane/main.go`: 테스트가 드러낸 mapper gap이 있을 때만 수정.

테스트 작성: 작성. `TestFleetStatusHTTPHandlerCombinesConnectionAndCapabilities` 또는 새 focused test에서 Edge별 domain agent summary를 검증한다.

중간 검증:

```bash
go test ./apps/control-plane/cmd/control-plane -run 'TestFleetStatusHTTPHandlerCombinesConnectionAndCapabilities' -count=1
```

### [API-2] Edge Operations Audit Isolation

문제: `apps/control-plane/cmd/control-plane/main.go:450`의 `/edges/{id}/operations`는 `registry.Commands(edgeID)`를 반환하고, `apps/control-plane/internal/wire/edge_registry.go:328`은 Edge ID로 command records를 필터링한다. 기존 `apps/control-plane/cmd/control-plane/main_test.go:643`은 edge-a command 하나만 seed해 cross-edge 섞임을 검증하지 않는다.

해결 방법: `TestEdgeOperationsHTTPHandlerListsAudit`에 `edge-b` command request/event/result를 추가한다. `/edges/edge-a/operations` 응답에 edge-b command가 없는지 확인하고, `/edges/edge-b/operations`도 조회해 edge-b command만 반환되는지 확인한다.

수정 파일 및 체크리스트:

- [ ] `apps/control-plane/cmd/control-plane/main_test.go`: edge-b command record fixture 추가.
- [ ] `apps/control-plane/cmd/control-plane/main_test.go`: edge-a/edge-b 양방향 operations isolation assertion 추가.
- [ ] `apps/control-plane/internal/wire/edge_registry.go`: 테스트가 기존 filtering gap을 드러낼 때만 수정.

테스트 작성: 작성. HTTP handler 레벨에서 Edge별 operations isolation을 검증한다.

중간 검증:

```bash
go test ./apps/control-plane/cmd/control-plane -run 'TestEdgeOperationsHTTPHandlerListsAudit' -count=1
```

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `apps/control-plane/cmd/control-plane/main_test.go` | API-1, API-2 |
| `apps/control-plane/cmd/control-plane/main.go` | API-1, API-2 only if tests reveal mapper gap |
| `apps/control-plane/internal/wire/edge_registry.go` | API-2 only if filtering gap appears |
| `apps/edge/internal/service/service.go` | API-1 only if domain agent source summary gap appears |
| `apps/edge/internal/service/service_test.go` | API-1 only if edge service behavior changes |

## 최종 검증

```bash
go test ./apps/control-plane/cmd/control-plane ./apps/control-plane/internal/wire ./apps/edge/internal/service -count=1
```

기대 결과: all packages pass. Go test cache output은 허용하지 않는다.

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