# SDD: Control Plane 운영 액션 표면

## 위치

- Milestone: `agent-roadmap/phase/control-plane-product-surface/milestones/control-plane-operator-actions.md`
- Phase: `agent-roadmap/phase/control-plane-product-surface/PHASE.md`

## 상태

[승인됨]

## SDD 잠금

- 상태: 해제
- 사용자 리뷰: 없음
- 잠금 항목:
  - 없음

## 문제 / 비목표

- 문제: read-only Control Plane 화면 위에 Core write endpoint를 붙이면 job, execution, runner 상태를 사용자가 직접 바꾸게 된다. 따라서 action source of truth, 확인 UX, 중복 제출 방지, 성공/실패/지연 상태 표시, evidence 연결을 구현 전에 고정해야 한다.
- 비목표:
  - 신규 Core API, proto, schema, persistence 계약을 만들지 않는다.
  - auth, 권한, 감사 로그, durable 운영 정책을 완성하지 않는다.
  - runner production end-to-end 운영 정책이나 배포 파이프라인을 완성하지 않는다.

## Source of Truth

| 영역 | 기준 | 메모 |
|------|------|------|
| Roadmap | `agent-roadmap/phase/control-plane-product-surface/milestones/control-plane-operator-actions.md` | Milestone Task, SDD gate, 완료 evidence 반영 기준 |
| Code | `services/core/internal/httpserver/routes.go` | 기존 Core HTTP route가 action endpoint source of truth다. |
| Code | `services/core/internal/httpserver/job_handlers.go` | `POST /api/v1/jobs`, `POST /api/v1/jobs/{id}/executions` 입력/상태 응답 기준 |
| Code | `services/core/internal/httpserver/runner_cicd_handlers.go` | runner-scoped cancel/report/log/artifact/self-update/status action 기준 |
| Code | `services/core/internal/httpserver/execution_handlers.go` | execution-scoped log/artifact append와 readback 기준 |
| Code | `proto/oto/runner.proto` | runner-scoped request/response JSON body의 field source of truth |
| Code | `apps/client/lib/src/app/core_connection_client.dart` | `OtoConsoleConfig.serverHttpUrl`과 client adapter 패턴의 source of truth |
| Code | `packages/flutter/oto_console/lib/src/oto_console_contract.dart` | UI package의 transport-free state/view contract 기준 |
| External Provider | 없음 | 외부 provider mutation은 없다. |
| User Decision | 없음 | 기존 Core 계약 소비와 conservative UX 표준선으로 결정 가능하다. |

## State Machine

| 상태 | 진입 조건 | 다음 상태 | 근거 |
|------|-----------|-----------|------|
| idle | 화면이 action 가능한 read state를 렌더링한다. | confirming, disabled | `OtoSurfaceSnapshot.data`, 대상 id 존재 여부 |
| disabled | 대상 id, 필수 입력, Core 연결, SDD에서 허용한 endpoint 조건이 부족하다. | idle | UI validation, `OtoConsoleConfig.serverHttpUrl` |
| confirming | job create, cancel, self-update처럼 lifecycle-changing action을 사용자가 시작한다. | submitting, idle | action surface confirmation UI |
| submitting | adapter가 기존 Core write endpoint에 단일 in-flight 요청을 보낸다. | succeeded, deferred, failed | `apps/client` write adapter result |
| succeeded | Core가 HTTP 2xx와 accepted/success 또는 created payload를 반환한다. | refreshing, idle | `writeResponse`, proto response `accepted`/`success`, job DTO |
| deferred | self-update처럼 Core가 HTTP 200이지만 `deferred=true`를 반환한다. | refreshing, idle | `selfUpdateToJSON(... deferred=true ...)` |
| failed | Core가 4xx/5xx, invalid JSON, timeout, mismatch, terminal-state rejection을 반환한다. | idle, confirming | Core error response, adapter error state |
| refreshing | action 이후 read surface를 다시 조회한다. | idle, failed | 기존 read adapter and surface snapshot |

## Interface Contract

- 계약 원문: 없음. 이 Milestone은 tracked code/proto의 기존 endpoint와 DTO를 그대로 소비하며, SDD에 별도 agent-contract 원문을 복제하지 않는다.
- 입력:
  - `serverHttpUrl`: `OtoConsoleConfig.serverHttpUrl`이며 모든 action adapter의 HTTP base URL이다.
  - `POST /api/v1/jobs`: body `id`, `name`, optional `run_request`; `run_request`는 기존 `RunRequest` JSON field를 따른다.
  - `POST /api/v1/jobs/{jobID}/executions`: body `id`; execution 생성 action을 노출하는 경우 기존 handler의 `id` 필드를 따른다.
  - `POST /api/v1/runners/{runnerID}/executions/{executionID}/cancel`: body `runner_id`, `execution_id`, optional `reason`; path/body id mismatch는 error로 표시한다.
  - `POST /api/v1/runners/{runnerID}/executions/{executionID}/report`: body `runner_id`, `job_id`, `execution_id`, `success`, `exit_code`, `message`, `step_events`; 운영자 UI에서 직접 report를 노출할 때는 destructive/system action으로 분리 표시한다.
  - `POST /api/v1/runners/{runnerID}/executions/{executionID}/logs`: body `runner_id`, `execution_id`, `line`.
  - `POST /api/v1/runners/{runnerID}/executions/{executionID}/artifacts`: body `runner_id`, `execution_id`, `name`, `path`.
  - `POST /api/v1/runners/{runnerID}/self-update`: body `runner_id`, `version`, `download_url`; `download_url`은 Core 기준으로 HTTPS만 허용한다.
- 출력:
  - `POST /api/v1/jobs`: HTTP 201이면 created job DTO를 success state로 변환한다. HTTP 400/409는 user-visible error state로 변환한다.
  - execution create/report/log/artifact/cancel: HTTP 2xx와 `accepted` 또는 `success`를 success state로 변환한다. `error_message` 또는 `error`가 있으면 error state로 표시한다.
  - self-update: `accepted=true`는 success, `deferred=true`는 deferred, `success=false`는 error state로 표시한다.
  - action 완료 후 read surface는 기존 read adapter로 refresh한다.
- 금지:
  - SDD 범위 안에서 신규 Core route, proto field, DB/persistence schema를 추가하지 않는다.
  - client가 optimistic terminal state를 Core source of truth보다 먼저 확정하지 않는다.
  - 동일 target/action에 대해 중복 in-flight request를 허용하지 않는다.
  - failed action을 자동 무한 재시도하지 않는다. 사용자가 다시 실행해야 한다.
  - credential, private release URL, enrollment token 원문을 tracked 문서나 UI 테스트 fixture에 남기지 않는다.

## Acceptance Scenarios

| ID | Milestone Task | Given | When | Then |
|----|----------------|-------|------|------|
| S01 | `action-sdd` | 현재 Milestone이 SDD 필요/잠금 상태다. | SDD gate를 확인한다. | SDD는 `[승인됨]`, SDD 잠금 `해제`, 사용자 리뷰 없음이며 Evidence Map이 모든 Task와 연결된다. |
| S02 | `job-create` | 사용자가 `id`, `name`, optional `run_request`를 입력하고 Core base URL이 유효하다. | job create action을 실행한다. | adapter는 `POST /api/v1/jobs`를 호출하고, 201/400/409/timeout을 success/error 상태로 표시하며 read surface refresh를 트리거한다. |
| S03 | `execution-actions` | 실행 대상 `runnerID`, `executionID`, optional `jobID`가 화면 state에 있다. | cancel/report/log/artifact action 중 하나를 실행한다. | adapter는 기존 runner/execution write endpoint를 호출하고 mismatch, terminal rejection, not found, accepted 결과를 구분해 표시한다. |
| S04 | `execution-actions` | action 이후 read surface가 최신 Core 상태를 다시 조회할 수 있다. | write action이 success 또는 deferred로 끝난다. | UI는 기존 read adapter로 jobs/executions/logs/artifacts 상태를 refresh하고 stale optimistic state를 남기지 않는다. |
| S05 | `runner-actions` | 사용자가 runner self-update target version과 HTTPS download URL을 입력한다. | self-update action을 실행한다. | adapter는 `POST /api/v1/runners/{runnerID}/self-update`를 호출하고 accepted/deferred/error state를 화면에 표시한다. |

## Evidence Map

| Scenario | Required Evidence | `agent-task` 연결 | `Spec Completion` 기대 |
|----------|-------------------|------------------|---------------------------|
| S01 | SDD file exists, `[승인됨]`, SDD 잠금 `해제`, no `USER_REVIEW.md`, `git diff --check` | `agent-task/m-control-plane-operator-actions/...` | `Spec Completion`에 S01, `action-sdd`, SDD path, no-user-review evidence를 남긴다. |
| S02 | `cd apps/client && flutter test`, `cd packages/flutter/oto_console && flutter test`, job create adapter/widget tests, related `server_test.go` route coverage reference | `agent-task/m-control-plane-operator-actions/...` | `Spec Completion`에 S02, `job-create`, adapter request/response state evidence를 남긴다. |
| S03 | `cd apps/client && flutter test`, `cd packages/flutter/oto_console && flutter test`, cancel/report/log/artifact action state tests, `services/core/internal/httpserver/server_test.go` existing route tests | `agent-task/m-control-plane-operator-actions/...` | `Spec Completion`에 S03, `execution-actions`, error/success state evidence를 남긴다. |
| S04 | UI refresh test proving write success/deferred triggers read adapter reload without optimistic terminal state | `agent-task/m-control-plane-operator-actions/...` | `Spec Completion`에 S04, `execution-actions`, refresh behavior evidence를 남긴다. |
| S05 | `cd apps/client && flutter test`, `cd packages/flutter/oto_console && flutter test`, self-update accepted/deferred/error tests, `TestHandleRunnerSelfUpdate` reference | `agent-task/m-control-plane-operator-actions/...` | `Spec Completion`에 S05, `runner-actions`, accepted/deferred/error evidence를 남긴다. |

## Cross-repo Dependencies

- 없음

## Drift Check

- [x] Milestone 기능 Task와 Acceptance Scenario가 일치한다.
- [x] Evidence Map이 plan/code-review/complete.log에서 검증 가능하다.
- [x] agent-contract를 쓰는 경우 SDD에 계약 원문을 복제하지 않았다.
- [x] 사용자 리뷰가 필요한 항목은 `USER_REVIEW.md`에만 남겼다.

## 사용자 리뷰 이력

- 없음

## 작업 컨텍스트

- 표준선: 기존 `services/core/internal/httpserver/**` route/handler/proto 계약을 source of truth로 삼고, `apps/client`는 HTTP adapter와 action state 변환을 소유하며, `packages/flutter/oto_console`은 transport-free UI contract와 action surface state를 소유한다.
- 후속 SDD: auth, 권한, 감사 로그, durable 운영 정책 또는 신규 Core API/schema/proto가 필요해질 때 별도 SDD로 분리한다.
