# SDD: Runner proto-socket transport hardening

## 위치

- Milestone: `agent-roadmap/phase/control-plane-product-surface/milestones/runner-proto-socket-transport-hardening.md`
- Phase: `agent-roadmap/phase/control-plane-product-surface/PHASE.md`

## 상태

[승인됨]

## SDD 잠금

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

## 문제 / 비목표

- 문제: OTO runner transport가 proto-socket push/session과 HTTP claim/polling compatibility 경로를 함께 갖고 있어, 기본 production runner loop, 상태 전이, cancel/status/self-update 방향, smoke evidence의 source of truth가 흐려질 수 있다.
- 비목표:
  - HTTP public operator API 전체 제거
  - production-grade 인증/권한/감사 로그
  - durable artifact storage와 장기 scheduler policy
  - iop Edge 직접 연결 경로 복원

## Source of Truth

| 영역 | 기준 | 메모 |
|------|------|------|
| Roadmap | `agent-roadmap/phase/control-plane-product-surface/milestones/runner-proto-socket-transport-hardening.md` | Milestone 기능 Task와 완료 반영 기준 |
| Code | `services/core/internal/runnersocket/server.go` | server-runner proto-socket register, heartbeat, RunRequest dispatch, report, cancel push 기준 |
| Code | `apps/runner/lib/oto/agent/registration_client.dart` | Dart runner socket session, heartbeat, pushed RunRequest 수신 기준 |
| Code | `apps/runner/lib/oto/agent/agent_runner.dart` | 기본 agent loop가 socket push session을 우선 소비하는 기준 |
| Code | `services/core/internal/httpserver/routes.go` | operator-facing HTTP API와 compatibility runner HTTP endpoint 경계 |
| Code | `proto/oto/runner.proto` | wire message 이름과 필드 계약 |
| Domain Rule | `agent-ops/rules/project/domain/agent/rules.md` | 기본 runner 경로는 proto-socket이고 HTTP polling은 compatibility fallback이라는 정책 기준 |
| External Provider | 없음 | 이 Milestone은 외부 provider write를 추가하지 않는다. |
| User Decision | 없음 | 기본 방향과 범위는 Milestone 및 domain rule로 결정되어 있다. |

## State Machine

| 상태 | 진입 조건 | 다음 상태 | 근거 |
|------|-----------|-----------|------|
| runner.accepted | `RegisterRunnerRequest`가 enrollment token, runner id, protocol version, capability 검증을 통과한다. | runner.online, runner.disconnected, runner.heartbeat_timeout | `runnerregistry.Register`, `runnersocket.RegisterRunnerRequest` handler |
| runner.online | socket session에서 register 직후 또는 heartbeat가 성공한다. | runner.disconnected, runner.heartbeat_timeout | `runnerregistry.Heartbeat`, socket disconnect listener, timeout loop |
| runner.disconnected | 현재 socket client가 닫히고 server가 해당 client를 현재 runner client로 보고 있다. | runner.accepted | duplicate/reconnect는 re-register를 통해 새 accepted record를 만든다. |
| job.queued | operator HTTP API가 `run_request`를 포함한 job을 생성한다. | job.running, job.failed, job.canceled | `cicdstate.Store`, socket dispatcher |
| job.running | socket dispatcher가 queued job을 claim하고 execution을 생성한 뒤 `RunRequest`를 push한다. | job.succeeded, job.failed, job.canceled | `runnersocket.DispatchQueuedJobs`, `handleExecutionReport`, `CancelJobExecution` |
| execution.running | job dispatch가 runner id를 execution owner로 고정하고 `RunRequest`를 runner에 보낸다. | execution.succeeded, execution.failed, execution.canceled | `cicdstate.Store`, runner report/cancel handlers |
| execution.canceled | operator HTTP cancel action이 store를 canceled로 전환하고 연결된 runner socket에 best-effort cancel message를 보낸다. | terminal | terminal report는 store transition guard와 report handler에서 모순 없이 거부/기록되어야 한다. |

## Interface Contract

- 계약 원문: 없음. Wire source는 `proto/oto/runner.proto`이며 SDD에는 원문을 복제하지 않는다.
- 입력:
  - `RegisterRunnerRequest`: runner identity, enrollment token, protocol version, capability, command catalog.
  - `HeartbeatRequest`: runner liveness와 health status.
  - `RunRequest`: server가 socket으로 runner에 push하는 job/execution 실행 입력. `pipeline_yaml` 또는 `pipeline_yaml_path` 중 정확히 하나만 유효하다.
  - `ExecutionReportRequest`: runner가 socket으로 보고하는 terminal execution 결과.
  - `LogAppendRequest` / `ArtifactReportRequest`: runner가 socket으로 보고하는 execution log/artifact metadata.
  - `CancelRunRequest`: operator HTTP cancel action 이후 server가 연결된 runner socket에 best-effort로 전달하는 cancel signal.
  - `RunnerStatusRequest` / `SelfUpdateRequest`: proto에는 남아 있지만 이 Milestone에서는 status source와 self-update 실행 범위를 명시적으로 고정한 뒤 구현 또는 범위 제외한다.
- 출력:
  - register/heartbeat/report/log/artifact response는 accepted/success 여부와 structured error를 반환한다.
  - operator-facing HTTP status/self-update response는 UI/operator가 읽는 상태와 deferral 결과를 반환한다.
- 금지:
  - 기본 agent loop를 HTTP `jobs/claim` polling 확장 대상으로 삼지 않는다.
  - HTTP runner job claim/report/log/artifact 경로를 기본 smoke나 production runner transport로 설명하지 않는다.
  - terminal execution/job 상태를 cancel 이후 runner report로 되돌리지 않는다.
  - generated protobuf 파일을 수동 수정하지 않는다.

## Acceptance Scenarios

| ID | Milestone Task | Given | When | Then |
|----|----------------|-------|------|------|
| S01 | `socket-contract` | Milestone과 agent domain rule이 proto-socket 기본 경로를 선언한다. | SDD와 domain rule을 확인한다. | SDD Acceptance Scenario와 domain rule 모두 HTTP polling을 기본 runner path로 설명하지 않는다. |
| S02 | `session-lifecycle` | 같은 runner id가 재연결하거나 heartbeat/disconnect가 발생한다. | socket server가 register, heartbeat, duplicate client close, disconnect/timeout을 처리한다. | server-assigned runner id reconciliation과 disconnect 상태 전이가 Go/Dart unit 또는 smoke에서 검증된다. |
| S03 | `dispatch-state` | queued job과 invalid `RunRequest` 후보가 있다. | socket dispatcher가 idle runner에 `RunRequest`를 push한다. | queued -> running -> terminal, invalid input -> failed, active execution gate가 socket dispatch 테스트로 검증된다. |
| S04 | `runner-actions` | operator가 cancel/status/self-update HTTP action을 호출한다. | server가 store, registry, runner socket message 방향을 적용한다. | cancel은 runner 실행과 store state 사이에 모순된 terminal report를 만들지 않고, status/self-update 범위가 구현 또는 명시적 범위 제외로 고정된다. |
| S05 | `compat-boundary` | HTTP runner claim/report/log/artifact compatibility 코드가 남아 있다. | 기본 agent loop, smoke, 테스트 이름/그룹을 정리한다. | `OtoServerSocketRegistrationClient`가 기본이고 HTTP polling 테스트는 compatibility 이름/그룹으로만 남는다. |
| S06 | `socket-smoke` | local/remote smoke 기준이 runner transport 완료 근거로 쓰인다. | agent smoke 문서와 테스트가 socket 연결 중심 evidence를 요구한다. | `cd services/core && go test ./...`, `cd apps/runner && dart analyze`, agent socket smoke test가 통과한다. |

## Evidence Map

| Scenario | Required Evidence | `agent-task` 연결 | `Spec Completion` 기대 |
|----------|-------------------|------------------|---------------------------|
| S01 | `agent-roadmap/sdd/.../SDD.md`와 `agent-ops/rules/project/domain/agent/rules.md`가 proto-socket 기본 경로와 HTTP compatibility 경계를 같은 방향으로 설명한다. | roadmap-only update | `socket-contract`: SDD와 domain rule에서 HTTP polling이 기본 경로가 아님을 확인 |
| S02 | Go/Dart unit 또는 smoke가 server-assigned runner id, duplicate connection close, disconnect/timeout, heartbeat failure를 검증한다. | `agent-task/m-runner-proto-socket-transport-hardening/01_socket_lifecycle` | `session-lifecycle`: lifecycle 상태 전이와 재연결 기준 검증 |
| S03 | socket dispatch 테스트가 queued -> running -> terminal, invalid input -> failed, active execution gate를 검증한다. | `agent-task/m-runner-proto-socket-transport-hardening/02+01_dispatch_state` | `dispatch-state`: socket dispatch 상태 전이 검증 |
| S04 | cancel/status/self-update 테스트가 operator HTTP action과 socket message/store state 방향을 검증하거나 self-update 실행 범위 제외를 명시한다. | `agent-task/m-runner-proto-socket-transport-hardening/03+01,02_runner_actions` | `runner-actions`: cancel terminal report 모순 방지와 action 범위 고정 |
| S05 | search/test evidence가 기본 agent loop와 기본 smoke에서 HTTP polling claim/report를 제거하고 compatibility 그룹으로만 남겼음을 보여준다. | `agent-task/m-runner-proto-socket-transport-hardening/04+01_compat_boundary` | `compat-boundary`: 기본 runner transport와 compatibility test naming 분리 |
| S06 | agent socket smoke와 local test rule/profile evidence가 socket-centered smoke 기준을 요구하고 통과한다. | `agent-task/m-runner-proto-socket-transport-hardening/05+02,03,04_socket_smoke` | `socket-smoke`: Go test, Dart analyze, agent socket smoke 통과 |

## 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`에만 남겼다.

## 사용자 리뷰 이력

- 없음

## 작업 컨텍스트

- 표준선: server/operator-facing HTTP API는 유지하고, runner-facing 내부 실행 transport는 proto-socket push/session으로 둔다.
- 표준선: HTTP runner claim/report/log/artifact 경로는 삭제 전 compatibility fallback으로만 남기며, 기본 agent loop와 기본 smoke evidence에서는 제외한다.
- 표준선: cancel은 operator HTTP action에서 store를 terminal canceled로 바꾸고 연결된 socket runner에 best-effort signal을 보낸다. runner-side cooperative stop이 부족하면 그 구현을 `runner-actions` plan에서 다루고, self-update의 실제 binary replacement는 범위 제외 또는 후속 Milestone으로 고정한다.
- 후속 SDD: 없음
