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

# Plan - REVIEW_API

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

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

## 배경

이 후속 계획은 `code_review_cloud_G07_0.log`의 Required finding만 처리한다. 현재 cancel endpoint는 body/path runner id와 runner 등록 여부는 확인하지만, 대상 execution이 실제 path runner 소유인지 확인하지 않은 채 취소를 수행한다.

## Roadmap Targets

- Milestone: `agent-roadmap/phase/message-based-build-agent/milestones/remote-run-lifecycle.md`
- Task ids:
  - `cancel-status-update`
- Completion mode: check-on-pass

## 범위 결정 근거

- 포함: runner-scoped cancel endpoint의 execution owner 검증, cross-runner cancel 회귀 테스트.
- 제외: Dart client helper, status endpoint, self-update endpoint, runner loop 동작. 이번 Required issue를 고치는 데 직접 필요하지 않다.
- user-review gate 사유 없음. 저장소 내부 코드와 테스트로 해결 가능하다.

## 구현 체크리스트

- [ ] `handleRunnerCancelExecution`이 cancel 대상 execution의 `RunnerID`를 path `runnerID`와 대조하고, 빈 owner 또는 다른 runner owner를 거부한다.
- [ ] `services/core/internal/httpserver/server_test.go`에 다른 등록 runner가 남의 execution을 cancel할 수 없고 job/execution 상태가 유지되는 회귀 테스트를 추가한다.
- [ ] Go HTTP server 검증을 실행하고 실제 stdout/stderr를 `CODE_REVIEW-*-G??.md`에 기록한다.
- [ ] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 수정 파일 요약

| 파일 | 항목 |
|------|------|
| `services/core/internal/httpserver/server.go` | REVIEW_API-1 |
| `services/core/internal/httpserver/server_test.go` | REVIEW_API-1 |

## 구현 항목

### [REVIEW_API-1] Runner-Owned Cancel Guard

문제: [services/core/internal/httpserver/server.go](/config/workspace/oto/services/core/internal/httpserver/server.go:1166)의 cancel flow는 path/body id 정규화 뒤 [services/core/internal/httpserver/server.go](/config/workspace/oto/services/core/internal/httpserver/server.go:1174)에서 execution을 읽지만, `exec.RunnerID`가 path `runnerID`와 같은지 확인하지 않는다. 등록된 다른 runner가 남의 execution id를 알면 취소할 수 있다.

해결 방법:

- `handleRunnerCancelExecution`에서 `store.GetExecution(execID)` 이후 `exec.RunnerID`를 확인한다.
- `exec.RunnerID == ""` 또는 `exec.RunnerID != runnerID`이면 cancel을 수행하지 않고 400 또는 403 계열 응답을 반환한다.
- 응답 body에는 `Success=false`와 명확한 `ErrorMessage`를 남긴다.
- cross-runner 요청 뒤 job/execution state가 여전히 `running`인지 테스트한다.

중간 검증:

```bash
cd services/core && go test ./internal/httpserver
```

## 최종 검증

```bash
cd services/core && go test ./...
```

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

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