<!-- task=m-runner-proto-socket-transport-hardening/03+01,02_runner_actions plan=0 tag=API -->

# Plan - API

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

`CODE_REVIEW-cloud-G07.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우는 것까지가 구현입니다. 검증을 실행하고 실제 stdout/stderr를 붙인 뒤 active 파일을 그대로 두고 review ready로 보고하세요. 선택된 SDD 결정 또는 Milestone `구현 잠금 > 결정 필요` 항목이 막으면 review stub의 `사용자 리뷰 요청` 섹션을 채우고 멈추며, 직접 사용자에게 질문하거나 `USER_REVIEW.md`, `complete.log`, archive를 만들지 않습니다. 환경/secret/서비스 문제나 증거 공백은 일반 follow-up 대상으로 기록합니다.

## 배경

operator HTTP action 표면은 cancel/status/self-update 요청을 받지만, 현 Milestone의 기본 runner transport는 proto-socket이다. cancel은 server에서 runner socket으로 내려갈 수 있는 `CancelExecution` 경로가 있고, status는 state read 성격이 강하며, self-update는 runner-side 실행 경로가 아직 HTTP compatibility로 남아 있다. 이 작업은 세 action의 방향을 socket-first 기준으로 명확히 하고, cancel/report 충돌을 상태 전이에서 안전하게 막는다.

## 사용자 리뷰 요청 흐름

사용자 리뷰 요청은 선택된 SDD 결정 또는 선택된 Milestone lock 결정이 실구현을 차단할 때만 active `CODE_REVIEW-cloud-G07.md`의 `사용자 리뷰 요청` 섹션에 기록한다. 구현 중 직접 사용자 프롬프트, 채팅 선택지, `request_user_input`, `USER_REVIEW.md` 생성은 금지이며, code-review가 검증과 파일 생성을 소유한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/control-plane-product-surface/milestones/runner-proto-socket-transport-hardening.md`
- Task ids:
  - `runner-actions`: cancel/status/self-update runner action의 socket/HTTP 방향을 정하고 취소와 terminal report 충돌을 막는다.
- Completion mode: check-on-pass
- Split dependency:
  - `01_socket_lifecycle` PASS complete evidence가 있어야 구현을 시작한다.
  - `02+01_dispatch_state` PASS complete evidence가 있어야 구현을 시작한다.

## Spec Targets

- SDD: `agent-roadmap/sdd/control-plane-product-surface/runner-proto-socket-transport-hardening/SDD.md`
- Acceptance scenarios:
  - `S04`: task=`runner-actions`; evidence=`cancel/status/self-update 요청 경로가 socket-first flow 또는 compatibility 제외 범위로 명시되고 검증된다.`
- Completion mode: spec-check-on-pass

## 분석 결과

### 읽은 파일

- `agent-test/local/rules.md`
- `agent-test/local/agent-smoke.md`
- `agent-test/local/framework-smoke.md`
- `agent-ops/rules/project/domain/agent/rules.md`
- `agent-ops/rules/project/domain/framework/rules.md`
- `agent-roadmap/phase/control-plane-product-surface/milestones/runner-proto-socket-transport-hardening.md`
- `agent-roadmap/sdd/control-plane-product-surface/runner-proto-socket-transport-hardening/SDD.md`
- `services/core/internal/httpserver/runner_cicd_handlers.go`
- `services/core/internal/runnersocket/server.go`
- `services/core/internal/cicdstate/store.go`
- `services/core/internal/httpserver/server_test.go`
- `apps/runner/lib/oto/agent/registration_client.dart`
- `apps/runner/lib/oto/agent/agent_runner.dart`
- `apps/runner/lib/oto/agent/oto_server_job_client.dart`
- `apps/runner/test/oto_server_connection_smoke_test.dart`
- `proto/oto/runner.proto`
- `Makefile`

### 테스트 환경 규칙

`test_env=local`을 적용했다. agent/core 양쪽 action behavior가 걸리므로 Go 전체 테스트, runner socket smoke, Dart analyzer를 실행한다. proto schema를 변경하면 `make proto`와 generated code 갱신이 추가로 필요하지만, 기본 plan은 기존 `CancelRunRequest`, `SelfUpdateRequest`, status HTTP DTO 범위에서 처리한다.

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

- operator cancel endpoint와 socket `CancelExecution` delivery가 하나의 흐름으로 검증되지 않는다.
- runner Dart session은 pushed `RunRequest`만 처리하며 `CancelRunRequest` listener가 없다.
- self-update request는 proto message와 HTTP client method가 있으나 socket consumer가 없어서 compatibility 제외 여부가 문서/테스트에서 불명확하다.
- cancel 이후 late terminal report가 cancelled state를 뒤집지 않는지 socket action scenario로 고정되어 있지 않다.

### 심볼 참조

`CancelExecution`, `CancelExecutionHandler`, `GetExecutionStatusHandler`, `RequestRunnerSelfUpdateHandler`, `CancelRunRequest`, `SelfUpdateRequest`, `OtoServerJobClient.cancelRun`, `requestSelfUpdate`를 중심으로 확인했다. removed/renamed symbol 없음.

### 분할 판단

이 작업은 session lifecycle과 dispatch/state guard가 선행되어야 의미가 있다. 후속 `05+02,03,04_socket_smoke`는 이 작업에서 확정한 action direction을 smoke pass criteria에 반영한다.

### 범위 결정 근거

이 plan은 action direction과 cancel/report 충돌만 다룬다. queued dispatch의 일반 상태 전이는 `02+01_dispatch_state`, HTTP fallback 명명과 compatibility boundary는 `04+01_compat_boundary`에서 다룬다.

### 빌드 등급

`cloud-G07`: action behavior가 operator HTTP endpoint, core state store, socket server, Dart runner session을 가로지른다. 반쪽 구현이 smoke에서 늦게 드러날 수 있어 full local core/agent verification이 필요하다.

## 구현 체크리스트

- [ ] 선행 `01_socket_lifecycle`, `02+01_dispatch_state` PASS complete evidence를 확인한다.
- [ ] [API-1] cancel/status action의 socket-first 방향과 상태 guard를 구현/검증한다. 검증: `cd services/core && go test -count=1 ./...`
- [ ] [API-2] runner-side cancel handling 또는 명시적 compatibility 제외를 구현/검증한다. 검증: `cd apps/runner && dart test test/oto_server_connection_smoke_test.dart`
- [ ] [API-3] self-update action을 socket-first 구현 또는 compatibility 제외 범위로 명확히 한다. 검증: `cd apps/runner && dart analyze`
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [API-1] cancel/status action의 socket-first 방향과 상태 guard

문제: [runner_cicd_handlers.go](/config/workspace/oto/services/core/internal/httpserver/runner_cicd_handlers.go:367)는 operator cancel endpoint를 제공하고, [server.go](/config/workspace/oto/services/core/internal/runnersocket/server.go:108)는 runner에게 socket cancel을 보낼 수 있다. 두 경로가 한 action flow로 묶여 있는지와 late terminal report guard가 부족하다.

해결 방법:

```go
// before: services/core/internal/runnersocket/server.go:108
func (s *Server) CancelExecution(ctx context.Context, runnerID string, executionID string) error {
    client := s.clientForRunner(runnerID)
    ...
}
```

operator cancel handler가 state cancel만 수행하는지, socket runner cancel delivery까지 수행해야 하는지 현재 wiring을 확인한다. SDD S04 기준으로 socket-first가 목표이면 cancel endpoint에서 runner id를 찾아 `CancelExecution`을 호출할 수 있도록 dependency 주입을 보강한다. status endpoint는 state read action으로 명시하고 socket delivery 대상이 아님을 테스트/문서에 남긴다.

수정 파일 및 체크리스트:

- [ ] cancel endpoint에서 state cancel과 runner socket cancel delivery 관계를 명확히 함
- [ ] cancel 후 late terminal report가 cancelled state를 뒤집지 않는 테스트 추가
- [ ] status endpoint가 socket command가 아니라 state query임을 테스트 이름/설명으로 명확히 함
- [ ] 필요 시 `services/core/internal/httpserver/server.go` 또는 handler dependency 보강

테스트 작성: 작성한다. 테스트 이름 후보: `TestCancelExecutionSendsSocketCancelToRunner`, `TestCancelPreventsLateTerminalReportFromOverwritingCancelledState`.

중간 검증:

```bash
cd services/core && go test -count=1 ./...
```

기대 결과: 모든 Go package 통과.

### [API-2] runner-side cancel handling 또는 compatibility 제외

문제: [registration_client.dart](/config/workspace/oto/apps/runner/lib/oto/agent/registration_client.dart:418)는 `CancelRunRequest` parser를 등록하지만 [agent_runner.dart](/config/workspace/oto/apps/runner/lib/oto/agent/agent_runner.dart:108)의 socket push loop는 `RunRequest`만 처리한다. socket cancel을 실제로 보낼 경우 runner가 수신 후 active execution을 중단하거나, 현재 Milestone에서 제외한다고 명시해야 한다.

해결 방법:

```dart
// before: apps/runner/lib/oto/agent/agent_runner.dart:108
await for (final runRequest in session.runRequests) {
  ...
}
```

가능한 범위에서 push session을 typed command stream으로 확장해 `CancelRunRequest`를 처리한다. active process cancel이 runner executor 구조상 큰 변경이면, 이번 Milestone에서는 cancel delivery evidence를 core socket send까지로 제한하고 runner process cancel은 compatibility/follow-up으로 명시한다. 단, 이 제외는 SDD S04 evidence에 남겨야 하며 smoke pass criteria와 충돌하면 사용자 리뷰 요청이 아니라 follow-up plan 대상으로 기록한다.

수정 파일 및 체크리스트:

- [ ] runner socket client가 `CancelRunRequest`를 수신할 수 있는지 확인/테스트
- [ ] active execution cancel handling 구현 가능성을 판단하고 구현 또는 범위 제외 기록
- [ ] core에서 cancel send 실패 시 operator action response/state 정책을 고정
- [ ] 필요 시 runner smoke에 cancel request 수신 검증 추가

테스트 작성: 작성한다. 테스트 이름 후보: `runner receives socket cancel for active execution`.

중간 검증:

```bash
cd apps/runner && dart test test/oto_server_connection_smoke_test.dart
```

기대 결과: socket smoke 통과.

### [API-3] self-update action 방향 명확화

문제: [proto/runner.proto](/config/workspace/oto/proto/oto/runner.proto:169)는 `SelfUpdateRequest`를 정의하고 [oto_server_job_client.dart](/config/workspace/oto/apps/runner/lib/oto/agent/oto_server_job_client.dart:344)는 HTTP self-update client를 제공한다. 그러나 server-side self-update socket dispatch와 runner consumer가 없다.

해결 방법:

이번 Milestone에서 self-update를 socket command로 구현할지, HTTP compatibility action으로 제외할지 결정하고 코드/테스트 이름/문서에 반영한다. 기본 추천은 product surface가 아직 operator action 수준이므로 self-update는 compatibility 제외로 명시하고, socket command 구현은 별도 Milestone으로 넘기는 것이다. 단, 기존 code path가 socket-first rule을 위반해 기본 path처럼 보이면 이름과 테스트를 compatibility로 정리한다.

수정 파일 및 체크리스트:

- [ ] self-update HTTP path가 compatibility/follow-up인지 명시
- [ ] socket-first smoke가 self-update를 필수 socket command로 오해하지 않게 정리
- [ ] 필요 시 roadmap SDD evidence에 excluded compatibility wording 업데이트
- [ ] Dart analyzer 통과 확인

테스트 작성: 변경 범위에 따라 작성한다. 문서/테스트 이름만 바꾸면 기존 smoke와 analyzer로 검증한다.

중간 검증:

```bash
cd apps/runner && dart analyze
```

기대 결과: analyzer issue 없음.

## 수정 파일 요약

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

## 최종 검증

```bash
cd services/core && go test -count=1 ./...
cd apps/runner && dart test test/oto_server_connection_smoke_test.dart
cd apps/runner && dart analyze
```

기대 결과: Go tests 통과, runner socket smoke 통과, analyzer issue 없음. 모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
