chore(agent-task): legacy IOP 아카이브 로그와 새 등록 클라이언트 경로를 추가한다
This commit is contained in:
parent
b81879b0dd
commit
919fcf37ba
6 changed files with 919 additions and 0 deletions
|
|
@ -0,0 +1,66 @@
|
|||
# Code review stub: runner registration legacy iop boundary split
|
||||
|
||||
## Review target
|
||||
|
||||
- Plan: `PLAN-cloud-G06.md`
|
||||
- Status: pending implementation
|
||||
|
||||
## Reviewer focus
|
||||
|
||||
- Confirm default runner production imports are OTO Server only.
|
||||
- Confirm legacy iop compatibility remains explicit and opt-in.
|
||||
- Confirm type aliases or transitional names do not preserve the same dependency leak.
|
||||
- Confirm tests still cover registration, job claim, reporting, cancellation, status, and self-update behavior.
|
||||
|
||||
## Required checks
|
||||
|
||||
- `cd apps/runner && dart analyze`
|
||||
- `cd apps/runner && dart test`
|
||||
|
||||
## Notes
|
||||
|
||||
This item is a boundary cleanup, not a protocol rewrite.
|
||||
|
||||
## 코드리뷰 결과
|
||||
|
||||
- 종합 판정: FAIL
|
||||
- 차원별 평가:
|
||||
- correctness: Pass
|
||||
- completeness: Fail
|
||||
- test coverage: Pass
|
||||
- API contract: Fail
|
||||
- code quality: Pass
|
||||
- plan deviation: Fail
|
||||
- verification trust: Fail
|
||||
- 발견된 문제:
|
||||
- Required: `apps/runner/lib/oto/agent/agent_runner.dart:23`에서 `DefaultAgentRunner`의 주입 콜백 타입이 여전히 `EdgeAgentSession`이고, `apps/runner/lib/oto/agent/agent_runner.dart:41`에서도 기본 runner 세션 타입을 `EdgeAgentSession`으로 선언합니다. 계획은 default runner가 neutral `AgentSession` 경계에 의존해야 한다고 요구했으므로 production path의 public/internal 타입 표면에 legacy Edge 명칭이 남아 있습니다. `runJobs` 콜백, 로컬 변수, 테스트 fake 타입을 `AgentSession` 기준으로 바꾸고, job loop 판별은 기존처럼 `OtoServerJobSession`으로 좁히세요.
|
||||
- Required: `apps/runner/lib/oto/agent/registration_client.dart:16`과 `apps/runner/lib/oto/agent/registration_client.dart:126`에서 neutral 파일이 legacy `EdgeEndpoint`와 `EdgeAgentSession` alias를 직접 소유합니다. 그 결과 default runner가 import하는 neutral module 자체가 legacy Edge 개념을 계속 노출합니다. `EdgeEndpoint`와 legacy alias는 `edge_registration_client.dart` 쪽으로 이동하거나 legacy 파일에서만 export하고, `registration_client.dart`는 `AgentSession`, `RegistrationClient`, `RegistrationResult`, `OtoServerRegistrationClient`, `OtoServerJobSession` 같은 OTO Server/neutral 경계만 노출하도록 정리하세요.
|
||||
- Required: `agent-task/control_plane_phase_exit_followups/02_legacy_iop_boundary/CODE_REVIEW-cloud-G06.md:1`이 구현 완료 스텁이 아니라 `Status: pending implementation` 상태로 남아 있고, 구현 체크리스트, 계획 대비 변경 사항, 주요 설계 결정, 실제 검증 stdout/stderr가 없습니다. 리뷰어가 로컬에서 `dart analyze`와 `dart test`를 재실행해 통과를 확인했지만, loop 계약상 구현 에이전트 소유 증거가 기록되지 않았으므로 verification trust를 PASS로 둘 수 없습니다. 후속 구현은 active review stub의 구현 소유 섹션을 실제 내용과 명령 출력으로 채워야 합니다.
|
||||
- 검증 참고:
|
||||
- `cd apps/runner && dart analyze` 통과 (`No issues found!`)
|
||||
- `cd apps/runner && dart test test/oto_agent_registration_test.dart test/oto_agent_migration_plan_test.dart test/oto_server_connection_smoke_test.dart` 통과 (`All tests passed!`)
|
||||
- `cd apps/runner && dart test` 통과 (`All tests passed!`, legacy iop smoke 1건은 opt-in skip)
|
||||
- 다음 단계: user-review gate는 트리거하지 않습니다. `PLAN-cloud-G06.md`와 `CODE_REVIEW-cloud-G06.md` 후속 루프를 작성해 위 Required 항목만 좁게 수정하고 검증 출력까지 기록합니다.
|
||||
|
||||
## 코드리뷰 전용 체크리스트
|
||||
|
||||
- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
|
||||
- [x] 판정과 `차원별 평가`, Required/Suggested/Nit 분류가 서로 일치한다.
|
||||
- [x] active `CODE_REVIEW-cloud-G06.md`를 `code_review_cloud_G06_0.log`로 아카이브한다.
|
||||
- [x] active `PLAN-cloud-G06.md`를 `plan_cloud_G06_0.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/control_plane_phase_exit_followups/02_legacy_iop_boundary/`를 archive로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
|
||||
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
|
||||
- [ ] PASS split 작업이면 이동 후 빈 active parent `agent-task/control_plane_phase_exit_followups/`를 제거하거나, 남은 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로 이동한다.
|
||||
|
||||
적용 결과:
|
||||
|
||||
- archived review: `agent-task/control_plane_phase_exit_followups/02_legacy_iop_boundary/code_review_cloud_G06_0.log`
|
||||
- archived plan: `agent-task/control_plane_phase_exit_followups/02_legacy_iop_boundary/plan_cloud_G06_0.log`
|
||||
- follow-up plan: `agent-task/control_plane_phase_exit_followups/02_legacy_iop_boundary/PLAN-cloud-G06.md`
|
||||
- follow-up review: `agent-task/control_plane_phase_exit_followups/02_legacy_iop_boundary/CODE_REVIEW-cloud-G06.md`
|
||||
- `git check-ignore -q -- <artifact>` 결과: 위 네 artifact 모두 exit code 1로 ignore되지 않음.
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
<!-- task=control_plane_phase_exit_followups/02_legacy_iop_boundary plan=1 tag=REVIEW_BOUNDARY -->
|
||||
|
||||
# Code Review Reference - REVIEW_BOUNDARY
|
||||
|
||||
> **[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.
|
||||
> Do not ask the user directly, present choices in chat, or call `request_user_input` during implementation; record the needed decision in `사용자 리뷰 요청` and stop for code-review.
|
||||
> 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-10
|
||||
task=control_plane_phase_exit_followups/02_legacy_iop_boundary, plan=1, tag=REVIEW_BOUNDARY
|
||||
|
||||
## 이 파일을 읽는 리뷰 에이전트에게
|
||||
|
||||
> **[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/control_plane_phase_exit_followups/02_legacy_iop_boundary/`에서 archive로 이동한다. WARN/FAIL이면 user-review gate를 확인한 뒤 다음 active plan/review 파일 또는 `USER_REVIEW.md`를 작성한다.
|
||||
4. PASS이고 task group이 `m-<milestone-slug>`이면 완료 이벤트 메타데이터를 보고한다. 이 task는 `m-*` task group이 아니므로 roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
|
||||
5. 적용 가능한 `코드리뷰 전용 체크리스트` 항목을 최종 `.log` 위치에서 체크한 뒤 보고한다.
|
||||
|
||||
---
|
||||
|
||||
## 구현 항목별 완료 여부
|
||||
|
||||
| 항목 | 완료 여부 |
|
||||
|------|---------|
|
||||
| [REVIEW_BOUNDARY-1] Neutral/legacy registration symbol boundary | [x] |
|
||||
| [REVIEW_BOUNDARY-2] Review evidence completion | [x] |
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [x] `registration_client.dart`와 `agent_runner.dart`의 production/neutral 타입 표면에서 legacy `EdgeAgentSession`/`EdgeEndpoint` 의존을 제거한다.
|
||||
- [x] `oto_agent_registration_test.dart`의 fake client/session과 one-shot registration test 명칭을 neutral naming으로 정리하고, legacy endpoint parsing coverage는 explicit legacy iop path로 옮기거나 유지 근거를 기록한다.
|
||||
- [x] `cd apps/runner && dart analyze`, agent 관련 focused test, `cd apps/runner && dart test`, deterministic `rg --sort path` boundary checks를 실행한다.
|
||||
- [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/control_plane_phase_exit_followups/02_legacy_iop_boundary/`를 archive로 이동하고 최종 archive 경로에서 이 체크리스트를 갱신한다.
|
||||
- [ ] PASS이고 task group이 `m-<milestone-slug>`이면 런타임이 읽을 완료 이벤트 메타데이터를 보고하고, roadmap 수정이나 `update-roadmap` 직접 호출을 하지 않는다.
|
||||
- [x] PASS split 작업이면 이동 후 빈 active parent `agent-task/control_plane_phase_exit_followups/`를 제거하거나, 남은 sibling/file이 있어 유지했다고 확인한다.
|
||||
- [ ] 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로 이동한다.
|
||||
|
||||
## 계획 대비 변경 사항
|
||||
|
||||
- `oto_agent_registration_test.dart`의 `EdgeEndpoint` 파싱 테스트는 `oto_iop_connection_smoke_test.dart`로 이동하지 않고 유지했다. 이유: `EdgeEndpoint` parsing은 legacy iop 연결 전에 URL을 파싱하는 순수 유닛 로직이며 외부 iop Edge 서버 없이도 실행 가능하다. 테스트 파일 상단에 `edge_registration_client.dart show EdgeEndpoint` 명시 import를 추가해 legacy boundary를 explicit하게 표시했다.
|
||||
- 기대 결과 "출력 없음"은 `agent_runner.dart`와 `registration_client.dart` 두 파일에서는 충족됐다. 테스트 파일에 `EdgeEndpoint` 참조가 남은 것은 위 근거에 따른 의도된 설계다.
|
||||
|
||||
## 주요 설계 결정
|
||||
|
||||
- `EdgeEndpoint`와 `EdgeAgentSession` typedef를 `registration_client.dart`에서 제거하고 `edge_registration_client.dart`에 로컬 정의로 이동했다. 이로써 neutral production boundary(`registration_client.dart`, `agent_runner.dart`)가 proto_socket/iop 코드에 의존하지 않는다.
|
||||
- `agent_runner.dart`의 `_runJobs`, constructor, local session 변수를 `AgentSession` 타입으로 변경했다. `OtoServerJobSession` type check는 유지된다.
|
||||
- `oto_agent_registration_test.dart`의 `FakeEdgeAgentSession` → `FakeAgentSession`, `FakeEdgeRegistrationClient` → `FakeRegistrationClient`로 rename했다.
|
||||
- `edge_registration_client.dart`는 `registration_client.dart`에서 `AgentSession`, `RegistrationResult`, `RegistrationClient`를 re-export하고 legacy-only인 `EdgeAgentSession` typedef와 `EdgeEndpoint`를 로컬 정의로 보유한다.
|
||||
|
||||
## 사용자 리뷰 요청
|
||||
|
||||
- 상태: 없음
|
||||
- 사유 유형: 없음
|
||||
- 결정 필요: 없음
|
||||
- 차단 근거: 없음
|
||||
- 실행한 검증/명령: 없음
|
||||
- 자동 후속 불가 이유: 없음
|
||||
- 재개 조건: 없음
|
||||
|
||||
## 리뷰어를 위한 체크포인트
|
||||
|
||||
- Default runner production path가 `registration_client.dart`를 import하더라도 `EdgeAgentSession`, `EdgeEndpoint`, iop/proto_socket symbol을 쓰지 않는지 확인한다.
|
||||
- `edge_registration_client.dart`와 `oto_iop_connection_smoke_test.dart`만 legacy iop/proto_socket boundary를 명시적으로 소유하는지 확인한다.
|
||||
- OTO Server registration/job tests가 registration, job claim, reporting, cancellation, status, self-update behavior를 계속 검증하는지 확인한다.
|
||||
- `검증 결과`의 stdout/stderr가 실제 소스와 재실행 결과에 부합하는지 확인한다.
|
||||
|
||||
## 검증 결과
|
||||
|
||||
### REVIEW_BOUNDARY-1 중간 검증
|
||||
```bash
|
||||
$ rg --sort path "EdgeAgentSession|EdgeEndpoint" apps/runner/lib/oto/agent/agent_runner.dart apps/runner/lib/oto/agent/registration_client.dart apps/runner/test/oto_agent_registration_test.dart
|
||||
apps/runner/test/oto_agent_registration_test.dart:import 'package:oto/oto/agent/edge_registration_client.dart' show EdgeEndpoint;
|
||||
apps/runner/test/oto_agent_registration_test.dart: group('EdgeEndpoint', () {
|
||||
apps/runner/test/oto_agent_registration_test.dart: test('EdgeEndpoint parses host and port from config edge url', () {
|
||||
apps/runner/test/oto_agent_registration_test.dart: final endpoint1 = EdgeEndpoint.parse('127.0.0.1:8080');
|
||||
apps/runner/test/oto_agent_registration_test.dart: final endpoint2 = EdgeEndpoint.parse('http://edge-server:9000');
|
||||
apps/runner/test/oto_agent_registration_test.dart: final endpoint3 = EdgeEndpoint.parse('https://secure-edge');
|
||||
apps/runner/test/oto_agent_registration_test.dart: final endpoint4 = EdgeEndpoint.parse('edge-no-port');
|
||||
```
|
||||
agent_runner.dart, registration_client.dart 모두 출력 없음. 테스트 파일의 EdgeEndpoint 참조는 edge_registration_client.dart 명시 import를 통한 의도된 참조 (계획 대비 변경 사항 참조).
|
||||
|
||||
```bash
|
||||
$ rg --sort path "proto_socket|oto/agent/iop|runtime.pb" apps/runner/lib/oto/agent/agent_runner.dart apps/runner/lib/oto/agent/registration_client.dart
|
||||
(출력 없음 — 기대 결과 충족)
|
||||
```
|
||||
|
||||
### REVIEW_BOUNDARY-2 중간 검증
|
||||
```bash
|
||||
$ rg --sort path "상태: 없음|dart analyze|dart test|EdgeAgentSession|EdgeEndpoint" agent-task/control_plane_phase_exit_followups/02_legacy_iop_boundary/CODE_REVIEW-cloud-G06.md
|
||||
- [x] `registration_client.dart`와 `agent_runner.dart`의 production/neutral 타입 표면에서 legacy `EdgeAgentSession`/`EdgeEndpoint` 의존을 제거한다.
|
||||
- [x] `cd apps/runner && dart analyze`, agent 관련 focused test, `cd apps/runner && dart test`, deterministic `rg --sort path` boundary checks를 실행한다.
|
||||
- 상태: 없음
|
||||
...
|
||||
```
|
||||
사용자 리뷰 요청 상태: 없음, 검증 명령 및 boundary check 출력 기록됨.
|
||||
|
||||
### 최종 검증
|
||||
```bash
|
||||
$ cd apps/runner && dart analyze
|
||||
Analyzing runner...
|
||||
No issues found!
|
||||
```
|
||||
|
||||
```bash
|
||||
$ cd apps/runner && dart test test/oto_agent_migration_plan_test.dart test/oto_agent_bootstrap_script_test.dart test/oto_agent_cli_test.dart test/oto_agent_config_test.dart test/oto_agent_registration_test.dart test/oto_server_connection_smoke_test.dart
|
||||
...
|
||||
00:09 +73: All tests passed!
|
||||
```
|
||||
|
||||
```bash
|
||||
$ cd apps/runner && dart test
|
||||
...
|
||||
00:10 +223 ~1: All tests 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.
|
||||
|
||||
## Sections and ownership
|
||||
|
||||
| Section | Owner | Note |
|
||||
|---------|-------|------|
|
||||
| Header comment, 개요, 리뷰 에이전트 지시 | Fixed at stub creation | Implementing agent must not modify or execute these |
|
||||
| 구현 항목별 완료 여부 (item names) | Fixed at stub creation | Implementing agent checks `[ ]` -> `[x]` only |
|
||||
| 구현 체크리스트 (item text/order) | Fixed at stub creation from plan | Implementing agent checks `[ ]` -> `[x]` only; final checkbox is mandatory before saving |
|
||||
| 코드리뷰 전용 체크리스트 | Review agent only | Implementing agent must not modify or check this section |
|
||||
| 계획 대비 변경 사항, 주요 설계 결정 | Implementing agent | Replace placeholder text with actual content |
|
||||
| 사용자 리뷰 요청 | Implementing agent | Keep `상태: 없음` unless user input is required to proceed |
|
||||
| 리뷰어를 위한 체크포인트 | Fixed at stub creation | Pre-filled from plan |
|
||||
| 검증 결과 (section headings + commands) | Fixed at stub creation | Implementing agent fills in command output only; command changes require a `계획 대비 변경 사항` entry |
|
||||
| 코드리뷰 결과 | Review agent appends | Not included in stub |
|
||||
|
||||
## 코드리뷰 결과
|
||||
|
||||
- 종합 판정: PASS
|
||||
- 차원별 평가:
|
||||
- correctness: Pass
|
||||
- completeness: Pass
|
||||
- test coverage: Pass
|
||||
- API contract: Pass
|
||||
- code quality: Pass
|
||||
- plan deviation: Pass
|
||||
- verification trust: Pass
|
||||
- 발견된 문제: 없음
|
||||
- 검증 참고:
|
||||
- `rg --sort path "EdgeAgentSession|EdgeEndpoint" apps/runner/lib/oto/agent/agent_runner.dart apps/runner/lib/oto/agent/registration_client.dart apps/runner/test/oto_agent_registration_test.dart`는 test file의 explicit legacy `EdgeEndpoint` import만 출력했다. 구현자가 `계획 대비 변경 사항`에 이 예외를 기록했고, production files인 `agent_runner.dart`와 `registration_client.dart`에는 출력이 없어 default runner boundary 조건은 충족한다.
|
||||
- `rg --sort path "proto_socket|oto/agent/iop|runtime.pb" apps/runner/lib/oto/agent/agent_runner.dart apps/runner/lib/oto/agent/registration_client.dart`는 출력 없음.
|
||||
- `cd apps/runner && dart analyze` 통과 (`No issues found!`)
|
||||
- `cd apps/runner && dart test test/oto_agent_migration_plan_test.dart test/oto_agent_bootstrap_script_test.dart test/oto_agent_cli_test.dart test/oto_agent_config_test.dart test/oto_agent_registration_test.dart test/oto_server_connection_smoke_test.dart` 통과 (`All tests passed!`)
|
||||
- `cd apps/runner && dart test` 통과 (`All tests passed!`, legacy iop smoke 1건 opt-in skip)
|
||||
- 다음 단계: PASS 종결. `complete.log` 작성 후 task directory를 archive로 이동한다.
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
# Complete - control_plane_phase_exit_followups/02_legacy_iop_boundary
|
||||
|
||||
## 완료 일시
|
||||
|
||||
2026-06-10
|
||||
|
||||
## 요약
|
||||
|
||||
Runner registration legacy iop boundary split follow-up completed after 2 review loops; final verdict PASS.
|
||||
|
||||
## 루프 이력
|
||||
|
||||
| Plan | Review | Verdict | 메모 |
|
||||
|------|--------|---------|------|
|
||||
| `plan_cloud_G06_0.log` | `code_review_cloud_G06_0.log` | FAIL | default runner and neutral registration boundary still exposed legacy Edge symbols; review evidence was incomplete |
|
||||
| `plan_cloud_G06_1.log` | `code_review_cloud_G06_1.log` | PASS | neutral production boundary now uses `AgentSession`; legacy Edge/proto-socket code is isolated to explicit legacy path |
|
||||
|
||||
## 구현/정리 내용
|
||||
|
||||
- `registration_client.dart` now owns the neutral OTO Server registration/session/job boundary without `EdgeAgentSession`, `EdgeEndpoint`, iop, or proto-socket imports.
|
||||
- `agent_runner.dart` uses `AgentSession` for injected job-loop and local session types while preserving `OtoServerJobSession` narrowing for the default OTO Server job loop.
|
||||
- `edge_registration_client.dart` contains the legacy iop Edge endpoint/session alias and proto-socket client path explicitly.
|
||||
- Runner agent tests use neutral fake session/client naming, while the remaining `EdgeEndpoint` unit coverage imports the explicit legacy path.
|
||||
|
||||
## 최종 검증
|
||||
|
||||
- `rg --sort path "EdgeAgentSession|EdgeEndpoint" apps/runner/lib/oto/agent/agent_runner.dart apps/runner/lib/oto/agent/registration_client.dart apps/runner/test/oto_agent_registration_test.dart` - PASS; production files produced no matches, and the only matches were the intentional explicit legacy `EdgeEndpoint` test import/usages recorded in the review log.
|
||||
- `rg --sort path "proto_socket|oto/agent/iop|runtime.pb" apps/runner/lib/oto/agent/agent_runner.dart apps/runner/lib/oto/agent/registration_client.dart` - PASS; no output.
|
||||
- `cd apps/runner && dart analyze` - PASS; `No issues found!`.
|
||||
- `cd apps/runner && dart test test/oto_agent_migration_plan_test.dart test/oto_agent_bootstrap_script_test.dart test/oto_agent_cli_test.dart test/oto_agent_config_test.dart test/oto_agent_registration_test.dart test/oto_server_connection_smoke_test.dart` - PASS; `All tests passed!`.
|
||||
- `cd apps/runner && dart test` - PASS; `All tests passed!` with legacy iop smoke skipped as opt-in.
|
||||
|
||||
## 잔여 Nit
|
||||
|
||||
- 없음
|
||||
|
||||
## 후속 작업
|
||||
|
||||
- 없음
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
# Runner registration legacy iop boundary split
|
||||
|
||||
## Metadata
|
||||
|
||||
- Status: planned
|
||||
- Task group: `control_plane_phase_exit_followups`
|
||||
- Plan id: `cloud-G06`
|
||||
- Review stub: `CODE_REVIEW-cloud-G06.md`
|
||||
- Roadmap target: first phase exit follow-up for `agent-roadmap/phase/independent-control-plane/PHASE.md`
|
||||
|
||||
## Problem
|
||||
|
||||
The default runner path has migrated toward OTO Server HTTP registration, but production runner code still exposes and imports legacy iop Edge registration concepts.
|
||||
|
||||
Examples:
|
||||
|
||||
- `apps/runner/lib/oto/agent/edge_registration_client.dart` contains both OTO Server HTTP clients and legacy proto-socket Edge registration.
|
||||
- `apps/runner/lib/oto/agent/agent_runner.dart` depends on types named around `EdgeAgentSession`.
|
||||
- The legacy iop smoke now runs only when `OTO_ENABLE_LEGACY_IOP_SMOKE=1`, but the production import boundary still makes legacy iop appear first-class.
|
||||
|
||||
This makes the first phase hard to close because "independent control plane" evidence is mixed with legacy Edge compatibility code.
|
||||
|
||||
## Rules read
|
||||
|
||||
- `agent-ops/rules/project/rules.md`
|
||||
- `agent-ops/rules/project/domain/agent/rules.md`
|
||||
- `agent-test/local/rules.md`
|
||||
- `agent-test/local/agent-smoke.md`
|
||||
|
||||
## Current evidence
|
||||
|
||||
- `apps/runner/lib/oto/agent/edge_registration_client.dart`
|
||||
- Contains `OtoServerRegistrationClient`, `OtoServerJobClient`, `RegistrationClient`, `EdgeRegistrationClient`, and session/result types together.
|
||||
- Imports legacy iop generated proto/proto-socket packages.
|
||||
- `apps/runner/lib/oto/agent/agent_runner.dart`
|
||||
- The default runner imports `edge_registration_client.dart`.
|
||||
- The default runner works against the OTO Server path but keeps legacy naming in its interface.
|
||||
- `apps/runner/test/oto_agent_registration_test.dart`
|
||||
- Already has broad coverage for OTO Server registration/job client behavior and fake runner sessions.
|
||||
- `apps/runner/test/oto_iop_connection_smoke_test.dart`
|
||||
- Is now opt-in via `OTO_ENABLE_LEGACY_IOP_SMOKE=1`.
|
||||
|
||||
## Scope
|
||||
|
||||
Separate the production OTO Server runner path from legacy iop Edge compatibility code without removing the legacy smoke itself.
|
||||
|
||||
Do not change protocol behavior, generated protobuf files, or enrollment semantics in this task.
|
||||
|
||||
## Proposed implementation
|
||||
|
||||
1. Introduce a neutral runner registration boundary.
|
||||
- Move shared OTO Server types to a neutral file, for example `registration_client.dart`.
|
||||
- Prefer names like `AgentSession`, `RegistrationClient`, `OtoServerRegistrationClient`, and `OtoServerJobClient`.
|
||||
- Keep public API changes localized to runner internals and tests.
|
||||
|
||||
2. Isolate legacy iop Edge code.
|
||||
- Keep `EdgeRegistrationClient` and proto-socket-only imports in a legacy-specific file or clearly marked compatibility section.
|
||||
- Ensure normal runner imports do not pull legacy iop generated proto/proto-socket code.
|
||||
- Keep `oto_iop_connection_smoke_test.dart` importing the legacy path explicitly.
|
||||
|
||||
3. Update default runner code and tests.
|
||||
- `DefaultAgentRunner` should depend on the neutral interface/session type.
|
||||
- Fake clients in `oto_agent_registration_test.dart` should use neutral naming.
|
||||
- Existing OTO Server registration/job tests should remain behaviorally equivalent.
|
||||
|
||||
4. Update documentation only where it names the default path.
|
||||
- Keep legacy smoke instructions opt-in.
|
||||
- Do not add broad migration docs unless needed to explain the new boundary.
|
||||
|
||||
## Expected files
|
||||
|
||||
- `apps/runner/lib/oto/agent/edge_registration_client.dart`
|
||||
- `apps/runner/lib/oto/agent/agent_runner.dart`
|
||||
- Possible new or renamed file under `apps/runner/lib/oto/agent/`
|
||||
- `apps/runner/test/oto_agent_registration_test.dart`
|
||||
- `apps/runner/test/oto_iop_connection_smoke_test.dart`
|
||||
- Possibly `apps/runner/test/oto_agent_migration_plan_test.dart`
|
||||
|
||||
## Verification
|
||||
|
||||
- `cd apps/runner && dart analyze`
|
||||
- `cd apps/runner && dart test test/oto_agent_registration_test.dart`
|
||||
- `cd apps/runner && dart test test/oto_agent_migration_plan_test.dart`
|
||||
- `cd apps/runner && dart test`
|
||||
|
||||
## Risks
|
||||
|
||||
- Renaming exported types may break hidden or downstream imports.
|
||||
- A file split can accidentally remove legacy smoke coverage if imports are not explicit.
|
||||
- Keeping compatibility aliases may be useful for one transition step, but they should not hide the default production boundary.
|
||||
|
||||
## Done when
|
||||
|
||||
- The default runner path no longer imports legacy iop proto/proto-socket code.
|
||||
- Legacy iop smoke remains available only as explicit migration evidence.
|
||||
- Runner analysis and tests pass locally.
|
||||
|
|
@ -0,0 +1,203 @@
|
|||
<!-- task=control_plane_phase_exit_followups/02_legacy_iop_boundary plan=1 tag=REVIEW_BOUNDARY -->
|
||||
|
||||
# Review follow-up: neutral runner registration boundary
|
||||
|
||||
## 이 파일을 읽는 구현 에이전트에게
|
||||
|
||||
이 plan의 구현이 끝나면 active `CODE_REVIEW-cloud-G06.md`의 구현 에이전트 소유 섹션을 실제 변경 내용과 검증 출력으로 채워야 한다. 구현 완료 후에도 active 파일은 그대로 두고 리뷰를 요청하며, `코드리뷰 결과`, 로그 rename, `complete.log`, archive 이동은 code-review 전용이다.
|
||||
|
||||
구현 중 사용자만 결정할 수 있는 범위 충돌, 사용자 소유 외부 환경, secret, 서비스 준비가 필요하면 사용자에게 직접 묻지 말고 `CODE_REVIEW-cloud-G06.md`의 `사용자 리뷰 요청` 섹션에 결정 항목, 근거, 실행 명령 출력, 자동 후속 불가 이유, 재개 조건을 기록한 뒤 멈춘다. 후속 에이전트가 명령 재실행이나 산출물 수집으로 해소할 수 있는 증거 공백은 사용자 리뷰 요청이 아니라 일반 follow-up 항목이다.
|
||||
|
||||
## 배경
|
||||
|
||||
이전 구현은 OTO Server HTTP 등록 코드를 `registration_client.dart`로 분리했고 analyzer와 runner 테스트는 통과했다. 하지만 default runner와 neutral registration 파일의 타입 표면에 legacy Edge 명칭이 남아 있어, 계획의 “production runner path에서 legacy iop Edge 개념 분리” 조건이 아직 충족되지 않았다. 또한 이전 active review file이 구현 증거를 담지 않은 pending stub이라 loop 완료 증거를 신뢰할 수 없다.
|
||||
|
||||
## 사용자 리뷰 요청 흐름
|
||||
|
||||
구현 중 blocker가 생기면 active review stub의 `사용자 리뷰 요청` 섹션을 사용한다. 구현 에이전트는 직접 사용자 질문, 채팅 선택지 제시, `request_user_input`, `USER_REVIEW.md` 생성, 로그 archive, `complete.log` 작성을 하지 않는다.
|
||||
|
||||
## 분석 결과
|
||||
|
||||
### 읽은 파일
|
||||
|
||||
- `agent-ops/rules/project/rules.md`
|
||||
- `agent-ops/rules/common/rules-roadmap.md`
|
||||
- `agent-roadmap/current.md`
|
||||
- `agent-ops/skills/common/router.md`
|
||||
- `agent-ops/skills/common/code-review/SKILL.md`
|
||||
- `agent-ops/skills/common/plan/SKILL.md`
|
||||
- `agent-ops/rules/project/domain/agent/rules.md`
|
||||
- `agent-test/local/rules.md`
|
||||
- `agent-test/local/agent-smoke.md`
|
||||
- `agent-task/control_plane_phase_exit_followups/02_legacy_iop_boundary/plan_cloud_G06_0.log`
|
||||
- `agent-task/control_plane_phase_exit_followups/02_legacy_iop_boundary/code_review_cloud_G06_0.log`
|
||||
- `apps/runner/lib/oto/agent/agent_runner.dart`
|
||||
- `apps/runner/lib/oto/agent/registration_client.dart`
|
||||
- `apps/runner/lib/oto/agent/edge_registration_client.dart`
|
||||
- `apps/runner/test/oto_agent_registration_test.dart`
|
||||
- `apps/runner/test/oto_server_connection_smoke_test.dart`
|
||||
- `apps/runner/test/oto_iop_connection_smoke_test.dart`
|
||||
- `apps/runner/test/oto_agent_migration_plan_test.dart`
|
||||
|
||||
### 테스트 환경 규칙
|
||||
|
||||
- test_env: local
|
||||
- 적용 규칙: `agent-test/local/rules.md`, `agent-test/local/agent-smoke.md`
|
||||
- agent 변경 필수 검증: `cd apps/runner && dart analyze`
|
||||
- agent 관련 테스트: `cd apps/runner && dart test test/oto_agent_migration_plan_test.dart test/oto_agent_bootstrap_script_test.dart test/oto_agent_cli_test.dart test/oto_agent_config_test.dart test/oto_agent_registration_test.dart test/oto_server_connection_smoke_test.dart`
|
||||
- 전체 회귀: `cd apps/runner && dart test`
|
||||
- 이 리뷰에서 확인한 결과: `dart analyze` 통과, `dart test test/oto_agent_registration_test.dart test/oto_agent_migration_plan_test.dart test/oto_server_connection_smoke_test.dart` 통과, `dart test` 통과.
|
||||
|
||||
### 테스트 커버리지 공백
|
||||
|
||||
- OTO Server registration, heartbeat, disconnect, job claim/report/cancel/status/self-update는 기존 `oto_agent_registration_test.dart`와 `oto_server_connection_smoke_test.dart`가 계속 커버한다.
|
||||
- legacy iop smoke는 `OTO_ENABLE_LEGACY_IOP_SMOKE=1` opt-in으로 유지된다.
|
||||
- default runner/neutral boundary에 legacy `EdgeAgentSession`, `EdgeEndpoint`, iop/proto_socket import가 남지 않았는지는 테스트 assertion보다 deterministic `rg --sort path` 검증으로 확인한다.
|
||||
|
||||
### 심볼 참조
|
||||
|
||||
- 변경 대상 legacy symbol: `EdgeAgentSession`, `EdgeEndpoint`
|
||||
- 현재 남은 참조:
|
||||
- `apps/runner/lib/oto/agent/agent_runner.dart:23`
|
||||
- `apps/runner/lib/oto/agent/agent_runner.dart:30`
|
||||
- `apps/runner/lib/oto/agent/agent_runner.dart:41`
|
||||
- `apps/runner/lib/oto/agent/registration_client.dart:16`
|
||||
- `apps/runner/lib/oto/agent/registration_client.dart:126`
|
||||
- `apps/runner/test/oto_agent_registration_test.dart:16`
|
||||
- `apps/runner/test/oto_agent_registration_test.dart:29`
|
||||
- `apps/runner/test/oto_agent_registration_test.dart:36`
|
||||
- `apps/runner/test/oto_agent_registration_test.dart:63`
|
||||
- `apps/runner/test/oto_agent_registration_test.dart:83`
|
||||
- `apps/runner/test/oto_agent_registration_test.dart:1290`
|
||||
- legacy-only references may remain in `apps/runner/lib/oto/agent/edge_registration_client.dart` and `apps/runner/test/oto_iop_connection_smoke_test.dart`.
|
||||
|
||||
### 분할 판단
|
||||
|
||||
단일 follow-up으로 유지한다. 수정은 하나의 agent boundary 안에서 default runner, neutral/legacy registration 파일, 관련 tests의 naming/import surface를 정합화하는 좁은 refactor다. API-vs-call-site 분리가 가능하지만 변경량이 작고 같은 검증 명령으로 확인되므로 split coordination 비용이 더 크다.
|
||||
|
||||
### 범위 결정 근거
|
||||
|
||||
프로토콜 동작, generated protobuf, OTO Server endpoint, heartbeat/disconnect 로직, remote job execution 로직은 변경하지 않는다. `edge_registration_client.dart`의 legacy iop 등록 기능은 유지하되, default runner와 neutral file이 legacy iop/proto_socket 또는 Edge 명칭을 기본 표면으로 노출하지 않도록만 정리한다. broad migration docs는 작성하지 않는다.
|
||||
|
||||
### 빌드 등급
|
||||
|
||||
route: `cloud-G06`. 동작 변경은 작지만 agent boundary, legacy compatibility, smoke evidence를 함께 검토해야 하며 이전 리뷰가 verification trust Fail을 기록했으므로 기존 cloud route를 유지한다.
|
||||
|
||||
## 구현 체크리스트
|
||||
|
||||
- [ ] `registration_client.dart`와 `agent_runner.dart`의 production/neutral 타입 표면에서 legacy `EdgeAgentSession`/`EdgeEndpoint` 의존을 제거한다.
|
||||
- [ ] `oto_agent_registration_test.dart`의 fake client/session과 one-shot registration test 명칭을 neutral naming으로 정리하고, legacy endpoint parsing coverage는 explicit legacy iop path로 옮기거나 유지 근거를 기록한다.
|
||||
- [ ] `cd apps/runner && dart analyze`, agent 관련 focused test, `cd apps/runner && dart test`, deterministic `rg --sort path` boundary checks를 실행한다.
|
||||
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.
|
||||
|
||||
### [REVIEW_BOUNDARY-1] Neutral/legacy registration symbol boundary
|
||||
|
||||
문제: `apps/runner/lib/oto/agent/agent_runner.dart:23`과 `apps/runner/lib/oto/agent/agent_runner.dart:41`이 default runner path에서 `EdgeAgentSession`을 사용한다. `apps/runner/lib/oto/agent/registration_client.dart:16`과 `apps/runner/lib/oto/agent/registration_client.dart:126`도 neutral file에 legacy `EdgeEndpoint`와 `EdgeAgentSession` alias를 둔다.
|
||||
|
||||
해결 방법:
|
||||
|
||||
```dart
|
||||
// before: apps/runner/lib/oto/agent/agent_runner.dart:23
|
||||
final Future<void> Function(AgentConfig, EdgeAgentSession)? _runJobs;
|
||||
|
||||
// after
|
||||
final Future<void> Function(AgentConfig, AgentSession)? _runJobs;
|
||||
```
|
||||
|
||||
```dart
|
||||
// before: apps/runner/lib/oto/agent/registration_client.dart:126
|
||||
typedef EdgeAgentSession = AgentSession;
|
||||
|
||||
// after
|
||||
// Remove this alias from the neutral file. If compatibility is needed,
|
||||
// define it in edge_registration_client.dart only.
|
||||
```
|
||||
|
||||
수정 파일 및 체크리스트:
|
||||
|
||||
- [ ] `apps/runner/lib/oto/agent/agent_runner.dart`: `_runJobs`, constructor `runJobs`, local `session` 타입을 `AgentSession`으로 변경한다. `OtoServerJobSession` type check는 유지한다.
|
||||
- [ ] `apps/runner/lib/oto/agent/registration_client.dart`: `EdgeEndpoint`와 `EdgeAgentSession`을 제거하고 neutral/OTO Server registration symbols만 남긴다.
|
||||
- [ ] `apps/runner/lib/oto/agent/edge_registration_client.dart`: legacy `EdgeEndpoint`와 필요 시 `typedef EdgeAgentSession = AgentSession`을 이 파일로 옮긴다. iop/proto_socket imports는 이 파일에만 남긴다.
|
||||
- [ ] `apps/runner/test/oto_agent_registration_test.dart`: default runner tests가 `AgentSession` 기준 fake를 사용하도록 업데이트한다.
|
||||
|
||||
테스트 작성: 새 테스트 파일은 만들지 않는다. 기존 unit/smoke tests를 업데이트하고, legacy endpoint parsing assertion이 필요하면 `oto_iop_connection_smoke_test.dart` 안의 explicit legacy path에 둔다.
|
||||
|
||||
중간 검증:
|
||||
|
||||
```bash
|
||||
rg --sort path "EdgeAgentSession|EdgeEndpoint" apps/runner/lib/oto/agent/agent_runner.dart apps/runner/lib/oto/agent/registration_client.dart apps/runner/test/oto_agent_registration_test.dart
|
||||
```
|
||||
|
||||
기대 결과: 출력 없음.
|
||||
|
||||
```bash
|
||||
rg --sort path "proto_socket|oto/agent/iop|runtime.pb" apps/runner/lib/oto/agent/agent_runner.dart apps/runner/lib/oto/agent/registration_client.dart
|
||||
```
|
||||
|
||||
기대 결과: 출력 없음.
|
||||
|
||||
### [REVIEW_BOUNDARY-2] Review evidence completion
|
||||
|
||||
문제: archived `code_review_cloud_G06_0.log`는 active 시점에 `Status: pending implementation` 상태였고 구현 체크리스트, 설계 결정, 검증 stdout/stderr가 없었다. code-review가 명령을 재실행해 통과를 확인했지만, loop artifact 자체가 구현 완료 증거를 담지 못했다.
|
||||
|
||||
해결 방법: 이번 후속 구현의 active `CODE_REVIEW-cloud-G06.md`에서 구현 항목별 완료 여부, 구현 체크리스트, 계획 대비 변경 사항, 주요 설계 결정, 사용자 리뷰 요청 상태, 검증 결과를 실제 내용으로 채운다. 검증 결과에는 요약이 아니라 실제 stdout/stderr 또는 저장한 출력 파일 경로와 생성 명령을 남긴다.
|
||||
|
||||
수정 파일 및 체크리스트:
|
||||
|
||||
- [ ] `agent-task/control_plane_phase_exit_followups/02_legacy_iop_boundary/CODE_REVIEW-cloud-G06.md`: 구현 소유 섹션을 전부 채운다.
|
||||
- [ ] 검증 명령을 변경했다면 `계획 대비 변경 사항`에 이유와 대체 명령을 기록한다.
|
||||
|
||||
테스트 작성: 테스트 코드 변경이 아니라 loop artifact completion이다. 검증은 최종 검증 명령과 `rg --sort path` 출력으로 확인한다.
|
||||
|
||||
중간 검증:
|
||||
|
||||
```bash
|
||||
rg --sort path "상태: 없음|dart analyze|dart test|EdgeAgentSession|EdgeEndpoint" agent-task/control_plane_phase_exit_followups/02_legacy_iop_boundary/CODE_REVIEW-cloud-G06.md
|
||||
```
|
||||
|
||||
기대 결과: 사용자 리뷰 요청이 `상태: 없음`이고, 실행한 검증 명령과 boundary check 출력이 active review stub에 기록되어 있다.
|
||||
|
||||
## 수정 파일 요약
|
||||
|
||||
| 파일 | 항목 |
|
||||
|------|------|
|
||||
| `apps/runner/lib/oto/agent/agent_runner.dart` | REVIEW_BOUNDARY-1 |
|
||||
| `apps/runner/lib/oto/agent/registration_client.dart` | REVIEW_BOUNDARY-1 |
|
||||
| `apps/runner/lib/oto/agent/edge_registration_client.dart` | REVIEW_BOUNDARY-1 |
|
||||
| `apps/runner/test/oto_agent_registration_test.dart` | REVIEW_BOUNDARY-1 |
|
||||
| `apps/runner/test/oto_iop_connection_smoke_test.dart` | REVIEW_BOUNDARY-1 |
|
||||
| `agent-task/control_plane_phase_exit_followups/02_legacy_iop_boundary/CODE_REVIEW-cloud-G06.md` | REVIEW_BOUNDARY-2 |
|
||||
|
||||
## 최종 검증
|
||||
|
||||
```bash
|
||||
cd apps/runner && dart analyze
|
||||
```
|
||||
|
||||
기대 결과: `No issues found!`
|
||||
|
||||
```bash
|
||||
cd apps/runner && dart test test/oto_agent_migration_plan_test.dart test/oto_agent_bootstrap_script_test.dart test/oto_agent_cli_test.dart test/oto_agent_config_test.dart test/oto_agent_registration_test.dart test/oto_server_connection_smoke_test.dart
|
||||
```
|
||||
|
||||
기대 결과: `All tests passed!`
|
||||
|
||||
```bash
|
||||
cd apps/runner && dart test
|
||||
```
|
||||
|
||||
기대 결과: `All tests passed!`; legacy iop smoke는 환경변수 미설정 시 opt-in skip이어야 한다.
|
||||
|
||||
```bash
|
||||
rg --sort path "EdgeAgentSession|EdgeEndpoint" apps/runner/lib/oto/agent/agent_runner.dart apps/runner/lib/oto/agent/registration_client.dart apps/runner/test/oto_agent_registration_test.dart
|
||||
```
|
||||
|
||||
기대 결과: 출력 없음.
|
||||
|
||||
```bash
|
||||
rg --sort path "proto_socket|oto/agent/iop|runtime.pb" apps/runner/lib/oto/agent/agent_runner.dart apps/runner/lib/oto/agent/registration_client.dart
|
||||
```
|
||||
|
||||
기대 결과: 출력 없음.
|
||||
|
||||
모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
|
||||
333
apps/runner/lib/oto/agent/registration_client.dart
Normal file
333
apps/runner/lib/oto/agent/registration_client.dart
Normal file
|
|
@ -0,0 +1,333 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:oto/oto/agent/agent_config.dart';
|
||||
import 'package:oto/oto/agent/oto_server_job_client.dart';
|
||||
import 'package:oto/oto/agent/oto/runner.pb.dart' as oto;
|
||||
import 'package:oto/oto/commands/command.dart';
|
||||
import 'package:oto/oto/commands/command_registry.dart';
|
||||
|
||||
const otoRunnerProtocolVersion = 'oto.runner.v1';
|
||||
const otoRunnerCapabilityName = 'oto-runner';
|
||||
const otoRunnerCapabilityVersion = '1.0.0';
|
||||
|
||||
class RegistrationResult {
|
||||
final bool accepted;
|
||||
final String? rejectReason;
|
||||
final String? nodeId;
|
||||
final String? alias;
|
||||
final Map<String, dynamic>? runtimeConfig;
|
||||
|
||||
RegistrationResult._({
|
||||
required this.accepted,
|
||||
this.rejectReason,
|
||||
this.nodeId,
|
||||
this.alias,
|
||||
this.runtimeConfig,
|
||||
});
|
||||
|
||||
String? get runnerId => nodeId;
|
||||
|
||||
factory RegistrationResult.accepted(
|
||||
String nodeId,
|
||||
String? alias,
|
||||
Map<String, dynamic> runtimeConfig,
|
||||
) {
|
||||
return RegistrationResult._(
|
||||
accepted: true,
|
||||
nodeId: nodeId,
|
||||
alias: alias,
|
||||
runtimeConfig: runtimeConfig,
|
||||
);
|
||||
}
|
||||
|
||||
factory RegistrationResult.rejected(String reason) {
|
||||
return RegistrationResult._(accepted: false, rejectReason: reason);
|
||||
}
|
||||
|
||||
factory RegistrationResult.fromOtoResponse(
|
||||
oto.RegisterRunnerResponse response,
|
||||
) {
|
||||
if (!response.accepted) {
|
||||
final reason = response.rejectReason.isNotEmpty
|
||||
? response.rejectReason
|
||||
: (response.hasError() ? response.error.message : 'rejected');
|
||||
return RegistrationResult.rejected(reason);
|
||||
}
|
||||
|
||||
return RegistrationResult.accepted(
|
||||
response.runnerId,
|
||||
response.alias.isEmpty ? null : response.alias,
|
||||
const {},
|
||||
);
|
||||
}
|
||||
|
||||
factory RegistrationResult.fromOtoJson(Map<String, dynamic> json) {
|
||||
final accepted = json['accepted'] == true;
|
||||
if (!accepted) {
|
||||
final err = json['error'];
|
||||
final errMessageFromObj = (err is Map) ? err['message'] : null;
|
||||
return RegistrationResult.rejected(
|
||||
(json['reject_reason'] ??
|
||||
json['rejectReason'] ??
|
||||
errMessageFromObj ??
|
||||
'rejected')
|
||||
.toString(),
|
||||
);
|
||||
}
|
||||
return RegistrationResult.accepted(
|
||||
(json['runner_id'] ?? json['runnerId'] ?? '').toString(),
|
||||
_emptyToNull((json['alias'] ?? '').toString()),
|
||||
const {},
|
||||
);
|
||||
}
|
||||
|
||||
static String? _emptyToNull(String value) => value.isEmpty ? null : value;
|
||||
}
|
||||
|
||||
/// OTO Server 등록 세션의 기본 인터페이스.
|
||||
///
|
||||
/// 세션 소유자는 종료 시 [close]를 호출해 transport를 정리할 책임이 있다.
|
||||
abstract class AgentSession {
|
||||
RegistrationResult get result;
|
||||
Future<void> close();
|
||||
}
|
||||
|
||||
abstract class OtoServerJobSession implements AgentSession {
|
||||
OtoServerJobClient get jobs;
|
||||
}
|
||||
|
||||
abstract class RegistrationClient {
|
||||
Future<AgentSession> openSession(AgentConfig config);
|
||||
|
||||
Future<RegistrationResult> register(AgentConfig config) async {
|
||||
final session = await openSession(config);
|
||||
try {
|
||||
return session.result;
|
||||
} finally {
|
||||
await session.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class OtoServerRegistrationClient extends RegistrationClient {
|
||||
final http.Client? _client;
|
||||
final List<String>? _commandTypes;
|
||||
final Duration _heartbeatInterval;
|
||||
|
||||
OtoServerRegistrationClient({
|
||||
http.Client? client,
|
||||
Iterable<String>? commandTypes,
|
||||
Duration heartbeatInterval = const Duration(seconds: 30),
|
||||
}) : _client = client,
|
||||
_commandTypes = commandTypes == null
|
||||
? null
|
||||
: List.unmodifiable(commandTypes),
|
||||
_heartbeatInterval = heartbeatInterval;
|
||||
|
||||
@override
|
||||
Future<AgentSession> openSession(AgentConfig config) async {
|
||||
final client = _client ?? http.Client();
|
||||
final shouldCloseClient = _client == null;
|
||||
try {
|
||||
final request = buildRegisterRunnerRequest(config);
|
||||
final response = await client
|
||||
.post(
|
||||
_registerUri(config.server.url),
|
||||
headers: {'content-type': 'application/json'},
|
||||
body: jsonEncode(_registerRequestJson(request)),
|
||||
)
|
||||
.timeout(const Duration(seconds: 5));
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||
throw HttpException(
|
||||
'Registration request failed with HTTP ${response.statusCode}',
|
||||
);
|
||||
}
|
||||
final decoded = jsonDecode(response.body);
|
||||
if (decoded is! Map<String, dynamic>) {
|
||||
throw const FormatException('Invalid registration response body');
|
||||
}
|
||||
final result = RegistrationResult.fromOtoJson(decoded);
|
||||
return _OtoServerAgentSession(
|
||||
result,
|
||||
client,
|
||||
shouldCloseClient,
|
||||
config.server.url,
|
||||
config.agent.id,
|
||||
heartbeatInterval: _heartbeatInterval,
|
||||
);
|
||||
} catch (_) {
|
||||
if (shouldCloseClient) {
|
||||
client.close();
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
oto.RegisterRunnerRequest buildRegisterRunnerRequest(AgentConfig config) {
|
||||
final request = oto.RegisterRunnerRequest()
|
||||
..enrollmentToken = config.agent.enrollmentToken
|
||||
..runnerId = config.agent.id
|
||||
..alias = config.agent.alias ?? ''
|
||||
..protocolVersion = otoRunnerProtocolVersion
|
||||
..capability = (oto.RunnerCapability()
|
||||
..name = otoRunnerCapabilityName
|
||||
..version = otoRunnerCapabilityVersion)
|
||||
..commandCatalog = (oto.CommandCatalogSummary()
|
||||
..commandTypes.addAll(_commandTypes ?? _defaultCommandTypes()));
|
||||
return request;
|
||||
}
|
||||
|
||||
static Uri _registerUri(String serverUrl) {
|
||||
var raw = serverUrl;
|
||||
if (!raw.contains('://')) {
|
||||
raw = 'http://$raw';
|
||||
}
|
||||
final uri = Uri.parse(raw);
|
||||
if (uri.host.isEmpty) {
|
||||
throw FormatException('Invalid server url: "$serverUrl"');
|
||||
}
|
||||
return uri.replace(path: _joinPath(uri.path, '/api/v1/runners/register'));
|
||||
}
|
||||
|
||||
static Uri heartbeatUri(String serverUrl, String runnerId) {
|
||||
var raw = serverUrl;
|
||||
if (!raw.contains('://')) {
|
||||
raw = 'http://$raw';
|
||||
}
|
||||
final uri = Uri.parse(raw);
|
||||
if (uri.host.isEmpty) {
|
||||
throw FormatException('Invalid server url: "$serverUrl"');
|
||||
}
|
||||
return uri.replace(
|
||||
path: _joinPath(uri.path, '/api/v1/runners/$runnerId/heartbeat'),
|
||||
);
|
||||
}
|
||||
|
||||
static Uri disconnectUri(String serverUrl, String runnerId) {
|
||||
var raw = serverUrl;
|
||||
if (!raw.contains('://')) {
|
||||
raw = 'http://$raw';
|
||||
}
|
||||
final uri = Uri.parse(raw);
|
||||
if (uri.host.isEmpty) {
|
||||
throw FormatException('Invalid server url: "$serverUrl"');
|
||||
}
|
||||
return uri.replace(
|
||||
path: _joinPath(uri.path, '/api/v1/runners/$runnerId/disconnect'),
|
||||
);
|
||||
}
|
||||
|
||||
static String _joinPath(String basePath, String endpointPath) {
|
||||
final base = basePath.endsWith('/')
|
||||
? basePath.substring(0, basePath.length - 1)
|
||||
: basePath;
|
||||
return '$base$endpointPath';
|
||||
}
|
||||
|
||||
static Map<String, dynamic> _registerRequestJson(
|
||||
oto.RegisterRunnerRequest request,
|
||||
) => {
|
||||
'enrollment_token': request.enrollmentToken,
|
||||
'runner_id': request.runnerId,
|
||||
'alias': request.alias,
|
||||
'protocol_version': request.protocolVersion,
|
||||
'capability': {
|
||||
'name': request.capability.name,
|
||||
'version': request.capability.version,
|
||||
},
|
||||
'command_catalog': {
|
||||
'command_types': request.commandCatalog.commandTypes.toList(),
|
||||
},
|
||||
};
|
||||
|
||||
List<String> _defaultCommandTypes() {
|
||||
registerAllCommands();
|
||||
return Command.catalogRows.map((row) => row['type']!).toList();
|
||||
}
|
||||
}
|
||||
|
||||
class _OtoServerAgentSession implements OtoServerJobSession {
|
||||
final http.Client _client;
|
||||
final bool _shouldCloseClient;
|
||||
final String _serverUrl;
|
||||
final String _runnerId;
|
||||
Timer? _heartbeatTimer;
|
||||
bool _closed = false;
|
||||
|
||||
@override
|
||||
final RegistrationResult result;
|
||||
|
||||
@override
|
||||
late final OtoServerJobClient jobs;
|
||||
|
||||
_OtoServerAgentSession(
|
||||
this.result,
|
||||
this._client,
|
||||
this._shouldCloseClient,
|
||||
this._serverUrl,
|
||||
this._runnerId, {
|
||||
required Duration heartbeatInterval,
|
||||
}) {
|
||||
jobs = OtoServerJobClient(
|
||||
serverUrl: _serverUrl,
|
||||
runnerId: _runnerId,
|
||||
client: _client,
|
||||
);
|
||||
if (result.accepted) {
|
||||
_heartbeatTimer = Timer.periodic(
|
||||
heartbeatInterval,
|
||||
(_) => _sendHeartbeat(),
|
||||
);
|
||||
scheduleMicrotask(() => _sendHeartbeat());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _sendHeartbeat() async {
|
||||
if (_closed) return;
|
||||
try {
|
||||
final uri = OtoServerRegistrationClient.heartbeatUri(
|
||||
_serverUrl,
|
||||
_runnerId,
|
||||
);
|
||||
final body = jsonEncode({
|
||||
'runner_id': _runnerId,
|
||||
'status': 1, // HEARTBEAT_STATUS_HEALTHY
|
||||
});
|
||||
await _client
|
||||
.post(uri, headers: {'content-type': 'application/json'}, body: body)
|
||||
.timeout(const Duration(seconds: 5));
|
||||
} catch (_) {
|
||||
// Ignore background errors
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_closed) return;
|
||||
_closed = true;
|
||||
|
||||
_heartbeatTimer?.cancel();
|
||||
_heartbeatTimer = null;
|
||||
|
||||
if (result.accepted) {
|
||||
try {
|
||||
final uri = OtoServerRegistrationClient.disconnectUri(
|
||||
_serverUrl,
|
||||
_runnerId,
|
||||
);
|
||||
await _client
|
||||
.post(uri, headers: {'content-type': 'application/json'})
|
||||
.timeout(const Duration(seconds: 5));
|
||||
} catch (_) {
|
||||
// Ignore disconnect errors
|
||||
}
|
||||
}
|
||||
|
||||
if (_shouldCloseClient) {
|
||||
_client.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue