<!-- task=m-remote-run-lifecycle/04+01,02_cancel_status_update plan=0 tag=API -->

# Plan - API

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

구현 완료의 마지막 단계는 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채우는 것이다. 검증 명령을 실행하고 실제 출력, 계획 대비 변경, 주요 설계 결정을 기록한 뒤 active 파일을 그대로 두고 리뷰 준비를 보고한다. 구현 중 사용자 전용 결정, 사용자 소유 환경/secret, 범위 충돌이 생기면 review stub의 `사용자 리뷰 요청` 섹션에 정확한 근거를 남기고 중단한다. 구현 에이전트는 사용자에게 직접 묻거나 선택지를 제시하거나 `request_user_input`을 호출하거나, `USER_REVIEW.md`/`complete.log`를 만들거나, 로그 아카이브를 수행하지 않는다.

## 배경

`proto/oto/runner.proto`에는 cancel, runner status, self-update 메시지가 이미 정의되어 있다. 현재 서버는 runner claim/report/log/artifact endpoint를 제공하지만 cancel/status/self-update HTTP endpoint와 running -> canceled 상태 전이가 없다. 이 계획은 기존 proto를 재사용해 원격 실행 생명주기의 운영 제어면을 닫는다.

## 사용자 리뷰 요청 흐름

구현 중 차단은 active review stub의 `사용자 리뷰 요청` 섹션에 기록한다. 직접 사용자 프롬프트는 금지되며, code-review가 요청 타당성을 검증하고 실제 `USER_REVIEW.md` 작성 여부를 결정한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/message-based-build-agent/milestones/remote-run-lifecycle.md`
- Task ids:
  - `cancel-status-update`: cancel, status, self-update의 최소 안정성 규칙이 정의되어 있다.
- Completion mode: check-on-pass

## 분석 결과

### 읽은 파일

- `agent-task/m-remote-run-lifecycle/01_run_request_pipeline/PLAN-cloud-G07.md`
- `agent-task/m-remote-run-lifecycle/02+01_step_log_stream/PLAN-local-G07.md`
- `agent-ops/rules/project/rules.md`
- `agent-ops/rules/common/rules-roadmap.md`
- `agent-ops/skills/common/plan/SKILL.md`
- `agent-ops/rules/project/domain/agent/rules.md`
- `agent-ops/rules/project/domain/core/rules.md`
- `agent-test/local/rules.md`
- `agent-test/local/agent-smoke.md`
- `agent-test/local/core-smoke.md`
- `agent-roadmap/current.md`
- `agent-roadmap/phase/message-based-build-agent/PHASE.md`
- `agent-roadmap/phase/message-based-build-agent/milestones/remote-run-lifecycle.md`
- `proto/oto/runner.proto`
- `services/core/internal/cicdstate/store.go`
- `services/core/internal/cicdstate/store_test.go`
- `services/core/internal/httpserver/server.go`
- `services/core/internal/httpserver/server_test.go`
- `services/core/internal/runnerregistry/registry.go`
- `apps/runner/lib/oto/agent/agent_runner.dart`
- `apps/runner/lib/oto/agent/edge_registration_client.dart`
- `apps/runner/lib/oto/agent/oto_server_job_client.dart`
- `apps/runner/test/oto_agent_registration_test.dart`
- `apps/runner/test/oto_server_connection_smoke_test.dart`

### 테스트 환경 규칙

- test_env: `local`.
- 읽은 프로파일: `agent-smoke`, `core-smoke`.
- Go service 변경은 `go test ./...`로 검증한다.
- Dart agent client 변경은 `dart analyze`와 agent/server smoke로 검증한다.
- 사전 검증으로 `go test ./...`, Dart agent/core/server smoke 테스트가 통과했다.

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

- store 상태 전이는 queued -> canceled만 허용하고 running -> canceled를 허용하지 않는다.
- 서버는 cancel/status/self-update proto 메시지를 HTTP endpoint로 받지 않는다.
- Dart job client는 cancel/status/self-update request helper가 없다.
- self-update는 실제 바이너리 교체를 수행하기 전에 active execution과 URL scheme을 확인하는 최소 안정성 규칙이 없다.

### 심볼 참조

- renamed/removed symbol: none.
- 변경 후보 call site: `validTransitions`, `Store.TransitionJob`, `Store.TransitionExecution`, `Server` route switch, `OtoServerJobClient`.

### 분할 판단

- split decision policy를 먼저 평가했다.
- 이 subtask는 `04+01,02_cancel_status_update`이므로 predecessor `01`, `02`가 필요하다.
- 현재 active `agent-task/m-remote-run-lifecycle/01_run_request_pipeline/complete.log`와 `agent-task/m-remote-run-lifecycle/02+01_step_log_stream/complete.log`가 없고 archive predecessor도 확인하지 않았다. predecessor `01`: missing, predecessor `02`: missing.
- `01`은 execution/job identity 계약을 제공하고, `02`는 runner loop의 active execution 개념을 제공한다. cancel/status/self-update는 이 생명주기 위에서 동작하므로 추가 숨은 의존성을 두지 않는다.

### 범위 결정 근거

- 이 계획은 HTTP/proto JSON boundary, store 상태 전이, Dart client helper, smoke 테스트를 다룬다.
- 실제 self-update 바이너리 다운로드/교체/재시작은 위험도가 높고 플랫폼별 정책이 필요하므로 범위 밖이다. 이 milestone에서는 최소 안정성 규칙과 accepted/deferred response만 정의한다.
- UI, auth, persistent DB migration은 범위 밖이다.

### 빌드 등급

- build: `cloud-G07`. Go server state machine과 Dart client protocol을 함께 바꾸며 remote lifecycle 제어 의미가 크다.
- review: `cloud-G07`. 상태 전이, HTTP status, Dart smoke가 서로 맞는지 교차 확인해야 한다.

## 구현 체크리스트

- [ ] `01_run_request_pipeline`과 `02+01_step_log_stream`의 `complete.log`가 존재하는지 확인하고 없으면 구현을 시작하지 않는다.
- [ ] running job/execution을 canceled로 전이할 수 있고 terminal state는 변경하지 않는다.
- [ ] cancel endpoint는 runner/job/execution id mismatch를 거부하고, 성공 시 job과 execution state를 모두 `canceled`로 맞춘다.
- [ ] status endpoint는 runner registry 상태와 현재/마지막 execution 상태를 반환한다.
- [ ] self-update endpoint는 active execution 중에는 defer/reject하고, download URL은 HTTPS만 허용한다. 실제 바이너리 교체는 하지 않는다.
- [ ] Dart job client가 cancel/status/self-update helper를 제공하고 snake_case/camelCase JSON을 파싱한다.
- [ ] Go service와 Dart agent/server smoke 테스트를 갱신한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 의존 관계 및 구현 순서

- Runtime dependency: sibling `01_run_request_pipeline` must produce `complete.log`.
- Runtime dependency: sibling `02+01_step_log_stream` must produce `complete.log`.
- 이 plan은 `agent-task/m-remote-run-lifecycle/04+01,02_cancel_status_update` 이름의 `+01,02` 의존성만 가진다. 추가 의존성을 plan 본문에 숨기지 않는다.

### [API-1] Cancellable State Transitions

문제: [services/core/internal/cicdstate/store.go](/config/workspace/oto/services/core/internal/cicdstate/store.go:17)의 `validTransitions`는 `StateRunning -> StateCanceled`를 허용하지 않는다.

Before:

```go
var validTransitions = map[string][]string{
	StateQueued:    {StateRunning, StateFailed, StateCanceled},
	StateRunning:   {StateSucceeded, StateFailed},
	StateSucceeded: nil,
	StateFailed:    nil,
	StateCanceled:  nil,
}
```

해결 방법: `StateRunning`에서 `StateCanceled`로 전이 가능하게 하고, job/execution을 함께 취소하는 store helper를 추가한다. 이미 succeeded/failed/canceled인 terminal state는 그대로 거부한다.

수정 파일 및 체크리스트:

- [ ] `services/core/internal/cicdstate/store.go`: running cancel 전이와 `CancelExecution` 또는 `CancelJobExecution` helper 추가.
- [ ] `services/core/internal/cicdstate/store_test.go`: queued/running cancel, terminal cancel rejection, job/execution 동시 갱신 테스트 추가.

중간 검증:

```bash
go test ./internal/cicdstate
```

### [API-2] HTTP Cancel And Status Endpoints

문제: [services/core/internal/httpserver/server.go](/config/workspace/oto/services/core/internal/httpserver/server.go:420)의 runner route switch는 claim/report/log/artifact만 처리하고 cancel/status endpoint가 없다.

해결 방법: 기존 proto JSON 필드를 쓰는 endpoint를 추가한다. 제안 route:

- `POST /api/v1/runners/{runner_id}/executions/{execution_id}/cancel`
- `GET /api/v1/runners/{runner_id}/status`

cancel은 `CancelRunRequest`의 runner/job/execution id가 path와 일치하는지 확인한다. status는 `RunnerStatusResponse`와 호환되는 JSON을 반환하고, 최소한 `accepted`, `runner_id`, `status`, `current_job_id`, `current_execution_id`, `message`를 포함한다.

수정 파일 및 체크리스트:

- [ ] `services/core/internal/httpserver/server.go`: route, request decoder, response writer 추가.
- [ ] `services/core/internal/httpserver/server_test.go`: cancel 성공/불일치/terminal rejection/status 조회 테스트 추가.

중간 검증:

```bash
go test ./internal/httpserver
```

### [API-3] Self-Update Stability Contract

문제: proto에는 `SelfUpdateRequest`/`SelfUpdateResponse`가 있으나 서버/runner 경계의 안정성 규칙이 없다.

해결 방법: `POST /api/v1/runners/{runner_id}/self-update` endpoint를 추가한다. request의 `download_url`은 HTTPS만 허용하고, active running execution이 있으면 `accepted=false`, `deferred=true`, `message`를 반환한다. active execution이 없으면 `accepted=true`, `deferred=false`, `target_version`과 `message`를 반환한다. 실제 다운로드와 교체는 하지 않는다.

수정 파일 및 체크리스트:

- [ ] `services/core/internal/httpserver/server.go`: self-update endpoint와 validation 추가.
- [ ] `services/core/internal/httpserver/server_test.go`: invalid URL, active execution defer, idle accepted 테스트 추가.

중간 검증:

```bash
go test ./internal/httpserver
```

### [API-4] Dart Lifecycle Client Helpers

문제: [apps/runner/lib/oto/agent/oto_server_job_client.dart](/config/workspace/oto/apps/runner/lib/oto/agent/oto_server_job_client.dart:68)는 claim/report/log/artifact만 제공한다.

해결 방법: `cancelRun`, `fetchRunnerStatus`, `requestSelfUpdate` helper와 response result classes를 추가한다. 기존 helper의 timeout/error handling 패턴을 재사용한다.

수정 파일 및 체크리스트:

- [ ] `apps/runner/lib/oto/agent/oto_server_job_client.dart`: lifecycle helper와 result parser 추가.
- [ ] `apps/runner/test/oto_agent_registration_test.dart`: request payload/response parser/error handling 테스트 추가.
- [ ] `apps/runner/test/oto_server_connection_smoke_test.dart`: 실제 server cancel/status/self-update smoke 추가.

중간 검증:

```bash
dart test test/oto_agent_registration_test.dart test/oto_server_connection_smoke_test.dart
```

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `services/core/internal/cicdstate/store.go` | API-1 |
| `services/core/internal/cicdstate/store_test.go` | API-1 |
| `services/core/internal/httpserver/server.go` | API-2, API-3 |
| `services/core/internal/httpserver/server_test.go` | API-2, API-3 |
| `apps/runner/lib/oto/agent/oto_server_job_client.dart` | API-4 |
| `apps/runner/test/oto_agent_registration_test.dart` | API-4 |
| `apps/runner/test/oto_server_connection_smoke_test.dart` | API-4 |

## 최종 검증

```bash
cd services/core && go test ./...
cd apps/runner && dart analyze
cd apps/runner && dart test test/oto_agent_registration_test.dart test/oto_server_connection_smoke_test.dart
```

예상 결과: 모든 명령이 exit code 0으로 종료한다.

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