refactor: control-plane-operator-actions migrated to archive and added control-plane-security-audit-surface

This commit is contained in:
toki 2026-06-19 17:42:48 +09:00
parent 7be0ab3fd0
commit 097f63f6d7
8 changed files with 1121 additions and 686 deletions

View file

@ -12,7 +12,7 @@ Flutter 콘솔의 read-only 화면 위에 기존 OTO Core write endpoint를 사
## 상태
[진행중]
[완료]
## 승격 조건
@ -43,17 +43,20 @@ Flutter 콘솔의 read-only 화면 위에 기존 OTO Core write endpoint를 사
기존 Control Plane read surface 위에 운영자가 실행할 수 있는 쓰기 액션의 안전한 UI/adapter 경계를 만든다.
- [x] [action-sdd] job/execution/runner write action의 source of truth, 상태 전이, 실패/재시도, 사용자 확인 UX, 검증 evidence를 SDD로 고정한다. 검증: `agent-roadmap/sdd/control-plane-product-surface/control-plane-operator-actions/SDD.md`가 승인되고 사용자 리뷰가 남아 있지 않다.
- [ ] [job-create] 기존 `POST /api/v1/jobs` 계약을 사용하는 job 생성 action adapter와 UI 상태를 연결한다. 검증: `cd apps/client && flutter test`, `cd packages/flutter/oto_console && flutter test`에서 job create action 상태 테스트가 통과한다.
- [ ] [execution-actions] 기존 execution/runner write endpoint를 사용하는 취소, report/log/artifact action 진입 상태를 연결한다. 검증: `cd apps/client && flutter test`, `cd packages/flutter/oto_console && flutter test`에서 execution action 상태 테스트가 통과한다.
- [ ] [runner-actions] 기존 runner self-update/status action 표면을 연결하고 실패/대기/완료 상태를 표시한다. 검증: `cd apps/client && flutter test`, `cd packages/flutter/oto_console && flutter test`에서 runner action 상태 테스트가 통과한다.
- [x] [job-create] 기존 `POST /api/v1/jobs` 계약을 사용하는 job 생성 action adapter와 UI 상태를 연결한다. 검증: `cd apps/client && flutter test`, `cd packages/flutter/oto_console && flutter test`에서 job create action 상태 테스트가 통과한다.
- [x] [execution-actions] 기존 execution/runner write endpoint를 사용하는 취소, report/log/artifact action 진입 상태를 연결한다. 검증: `cd apps/client && flutter test`, `cd packages/flutter/oto_console && flutter test`에서 execution action 상태 테스트가 통과한다.
- [x] [runner-actions] 기존 runner self-update/status action 표면을 연결하고 실패/대기/완료 상태를 표시한다. 검증: `cd apps/client && flutter test`, `cd packages/flutter/oto_console && flutter test`에서 runner action 상태 테스트가 통과한다.
## 완료 리뷰
- 상태: 없음
- 요청일: 없음
- 완료 근거: 없음
- 검토 항목: 없음
- 리뷰 코멘트: 없음
- 상태: 통과
- 요청일: 2026-06-19
- 완료 근거:
- `apps/client`의 Core write adapter와 `OtoClientApp` action wiring이 job create, execution cancel/report/log/artifact, runner self-update 상태를 연결한다.
- `packages/flutter/oto_console`의 jobs/executions/runners surface가 action 입력, disabled/submitting/success/error/deferred 상태를 렌더링한다.
- `cd apps/client && flutter test`, `cd packages/flutter/oto_console && flutter test`, `git diff --check`가 통과했다.
- 검토 항목: archive 가능.
- 리뷰 코멘트: 코드레벨 검토에서 작은 endpoint fallback/test analyzer 이슈를 수정했고, 남은 구현 잠금 또는 결정 필요 항목 없음.
## 범위 제외

View file

@ -24,9 +24,12 @@
- [완료] Control Plane API 데이터 바인딩
- 경로: `agent-roadmap/archive/phase/control-plane-product-surface/milestones/control-plane-api-data-binding.md`
- 요약: UI 스켈레톤에 기존 OTO Core runner/job/execution/log/artifact API의 read path를 연결하고, 실제 데이터가 비어 있거나 실패할 때의 상태 표현을 검증한다.
- [진행중] Control Plane 운영 액션 표면
- 경로: `agent-roadmap/phase/control-plane-product-surface/milestones/control-plane-operator-actions.md`
- [완료] Control Plane 운영 액션 표면
- 경로: `agent-roadmap/archive/phase/control-plane-product-surface/milestones/control-plane-operator-actions.md`
- 요약: 기존 OTO Core write endpoint를 UI/adapter action 표면으로 연결하되, 상태 전이와 실패/확인 UX는 SDD로 먼저 잠근다.
- [스케치] Control Plane 운영 보안·감사 표면
- 경로: `agent-roadmap/phase/control-plane-product-surface/milestones/control-plane-security-audit-surface.md`
- 요약: write action 표면 이후 필요한 인증/권한, 감사 로그, 민감 정보 마스킹, UI 노출 경계를 SDD와 구현 가능한 Task로 구체화한다.
## Phase 경계

View file

@ -0,0 +1,77 @@
# Milestone: Control Plane 운영 보안·감사 표면
## 위치
- Roadmap: `agent-roadmap/ROADMAP.md`
- Phase: `agent-roadmap/phase/control-plane-product-surface/PHASE.md`
## 목표
Control Plane write action 표면 위에 운영 권한, 확인 가능한 감사 흔적, 보안 경계를 붙일 다음 구현 단위를 정의한다.
완료 후 사용자는 인증/권한과 감사 로그의 source of truth, UI 노출 범위, Core 계약 변경 필요 여부를 구현 계획으로 전환할 수 있어야 한다.
## 상태
[스케치]
## 승격 조건
- [ ] 인증/권한의 최소 제품 범위와 책임 경계를 확정한다.
- [ ] 감사 로그의 source of truth, 보존 범위, UI 노출 범위를 확정한다.
- [ ] 신규 Core API/schema/proto가 필요한지 기존 endpoint 조합으로 충분한지 판정한다.
- [ ] SDD 작성 범위와 사용자 결정 필요 항목을 분리한다.
- [ ] 구현 가능한 기능 Task와 검증 기준을 `[계획]` 상태로 정리한다.
## 구현 잠금
- 상태: 잠금
- SDD: 필요
- SDD 문서: `agent-roadmap/sdd/control-plane-product-surface/control-plane-security-audit-surface/SDD.md`
- SDD 사유: 인증/권한, 감사 로그, write action 책임 경계는 API/schema/UI 상태와 사용자 승인 gate에 영향을 주므로 구현 전에 source of truth와 상태 전이를 고정해야 한다.
- 잠금 해제 조건:
- [ ] SDD 상태가 `[승인됨]`이고 SDD 잠금이 해제되어 있다.
- [ ] SDD `USER_REVIEW.md`가 없거나 승인/해결되었다.
- [ ] Acceptance Scenario가 Milestone 기능 Task와 연결되어 있다.
- [ ] Evidence Map이 plan의 `Spec Targets`와 완료 시 `Spec Completion`으로 검증 가능하게 연결되어 있다.
- 결정 필요:
- 최소 인증 모델과 권한 역할을 어디까지 이 Phase에 포함할지 결정한다.
- 감사 로그의 저장 책임이 Core in-memory/event surface인지 durable persistence 후보인지 결정한다.
- 사용자에게 노출할 감사 이벤트 범위와 민감 정보 마스킹 기준을 결정한다.
## 범위
- `services/core` write endpoint 주변의 인증/권한 hook 또는 audit source of truth 필요 여부를 정의한다.
- `apps/client``packages/flutter/oto_console`에서 운영 action 결과와 감사/권한 상태를 표현할 UI 범위를 정의한다.
- 기존 Control Plane 제품 표면 흐름을 기준으로 하며, production-grade identity provider 연동은 기본 구현 범위로 확정하지 않는다.
## 기능
### Epic: [security-audit] 운영 보안·감사 경계 정의
write action 이후 필요한 운영 보안과 감사 capability를 구현 가능한 계획으로 구체화한다.
- [ ] [security-sdd] 인증/권한, 감사 source of truth, 민감 정보 마스킹, 실패/거부 UX, 검증 evidence를 SDD로 고정한다.
- [ ] [auth-scope] 최소 인증/권한 모델과 UI disabled/error 상태 범위를 기능 Task로 확정한다.
- [ ] [audit-scope] 감사 이벤트 기록/조회/표시 범위와 후속 persistence 후보를 기능 Task로 확정한다.
## 완료 리뷰
- 상태: 없음
- 요청일: 없음
- 완료 근거: 없음
- 검토 항목: 없음
- 리뷰 코멘트: 없음
## 범위 제외
- production identity provider, 장기 secret rotation, 조직/테넌트 모델 완성은 이 스케치에서 바로 구현하지 않는다.
- durable audit storage를 구현 범위로 확정하지 않는다. 필요하면 별도 Milestone 또는 SDD 결정으로 분리한다.
- 기존 runner production job 운영 정책 전체를 완성하지 않는다.
## 작업 컨텍스트
- 관련 경로: `services/core/internal/httpserver/**`, `apps/client/**`, `packages/flutter/oto_console/**`
- 표준선(선택): 기존 Core route와 Flutter action surface를 source of truth 후보로 먼저 검토하고, API/schema 확장이 필요할 때만 별도 계약 변경으로 분리한다.
- 선행 작업: `Control Plane 운영 액션 표면`
- 후속 작업: 운영 대시보드 고도화, durable persistence UI
- 확인 필요: `구현 잠금 > 결정 필요` 항목과 SDD 사용자 리뷰로 분리한다.

View file

@ -0,0 +1,132 @@
# SDD: Control Plane 운영 보안·감사 표면
## 위치
- Milestone: `agent-roadmap/phase/control-plane-product-surface/milestones/control-plane-security-audit-surface.md`
- Phase: `agent-roadmap/phase/control-plane-product-surface/PHASE.md`
## 상태
[승인됨]
## SDD 잠금
- 상태: 해제
- 사용자 리뷰: 없음
- 잠금 항목:
- [x] [D01] 최소 인증 모델과 권한 역할의 Phase 포함 범위를 operator/admin 단일 역할과 표시 가능한 token reference 기준으로 확정했다.
- [x] [D02] 감사 로그는 운영 action metadata 감사 범위로 두고, Core in-memory/event surface 또는 테스트 가능한 adapter 경계까지만 이번 Milestone 범위로 확정했다.
- [x] [D03] 감사 UI는 운영 판단용 metadata만 노출하고, 로그/아티팩트/토큰류 원문과 민감 error payload는 복제 노출하지 않는 기준으로 확정했다.
## 문제 / 비목표
- 문제: Control Plane write action 표면은 runner self-update, execution cancel/report/log/artifact 같은 운영 변경을 노출하지만, 현재 설계에는 operator identity, 권한 실패 의미, 감사 흔적의 source of truth, 민감 정보 노출 경계가 고정되어 있지 않다.
- 비목표:
- production identity provider, 조직/테넌트 모델, 장기 secret rotation을 이 Milestone에서 완성하지 않는다.
- durable audit storage 구현을 기본 범위로 확정하지 않는다. file/DB 기반 감사 저장, retention, 삭제 정책은 후속 Milestone으로 분리한다.
- 원격 빌드 로그 원문, artifact 파일 내용, raw log line, production-grade 사용자 로그인 히스토리를 감사 로그 범위에 포함하지 않는다.
- 기존 runner production job 운영 정책 전체를 재설계하지 않는다.
## Source of Truth
| 영역 | 기준 | 메모 |
|------|------|------|
| Roadmap | `agent-roadmap/phase/control-plane-product-surface/milestones/control-plane-security-audit-surface.md` | Milestone 상태, 기능 Task, 구현 잠금의 장기 원장이다. |
| Code | `services/core/internal/httpserver/routes.go` | Core HTTP write route의 현재 source of truth다. |
| Code | `services/core/internal/httpserver/runner_cicd_handlers.go` | runner self-update, execution cancel/report/log/artifact write handler의 현재 검증·상태 변경 기준이다. |
| Code | `apps/client/lib/src/app/core_connection_client.dart` | Flutter client가 Core write endpoint를 호출하고 success/deferred/failed로 해석하는 기준이다. |
| Code | `packages/flutter/oto_console/lib/src/oto_console_contract.dart` | console config, auth token reference, action draft/result contract의 UI 패키지 기준이다. |
| External Provider | 없음 | 이 SDD는 외부 provider write를 추가하지 않는다. |
| User Decision | D01 | production identity provider는 후속으로 분리하고, operator/admin 단일 역할과 표시 가능한 token reference만 이번 Milestone 기준으로 삼는다. |
| User Decision | D02 | 감사 로그는 operator/runner write action의 actor, action, target, outcome, timestamp, optional reason metadata로 한정하고, durable persistence는 후속 Milestone으로 분리한다. |
| User Decision | D03 | UI에는 action, actor reference, target id, outcome, timestamp, optional reason만 노출하고 token, credential, private URL credential, raw log line, artifact 내용/full path, 민감 error payload는 노출하지 않는다. |
## State Machine
| 상태 | 진입 조건 | 다음 상태 | 근거 |
|------|-----------|-----------|------|
| SDD draft | SDD 문서가 생성되고 사용자 결정 항목이 식별됨 | User review pending | `SDD.md`, `USER_REVIEW.md` |
| User review pending | D01-D03이 사용자 답변으로 확정됨 | SDD approved candidate | `USER_REVIEW.md` 해결 로그 |
| SDD approved candidate | Acceptance Scenario와 Evidence Map이 Milestone 기능 Task와 연결됨 | Plan ready | `SDD.md`, Milestone `구현 잠금` |
| Operator action requested | UI 또는 adapter가 Core write endpoint를 호출함 | Authorized, rejected, deferred, or failed | `core_connection_client.dart`, Core HTTP response |
| Authorized action accepted | Core가 기존 상태 검증과 권한 기준을 통과함 | Audit event candidate | `runner_cicd_handlers.go`, D01 |
| Action rejected or failed | 권한, ownership, invalid input, missing runner/execution, terminal state 등으로 거부됨 | Audit event candidate | Core error response, D01-D03 |
| Audit event candidate | action outcome과 actor/target/reason metadata가 생성됨 | Captured in in-memory/event surface or withheld | D02-D03 |
## Interface Contract
- 계약 원문: 없음
- 입력:
- `actor`: operator 또는 runner actor identity. 구체 모델은 D01에서 확정한다.
- `role`: write action 허용 여부를 판정할 최소 역할. 구체 범위는 D01에서 확정한다.
- `action`: `create-job`, `create-execution`, `self-update-runner`, `cancel-execution`, `report-execution`, `append-log`, `append-artifact` 중 하나.
- `target`: runner id, job id, execution id, artifact/log 대상 등 action 대상 식별자.
- `reason`: cancel/self-update 같은 operator action에 남길 선택적 사유.
- `request_body`: 기존 JSON request body 또는 후속 schema 확장안.
- 출력:
- `status`: `accepted`, `deferred`, `succeeded`, `rejected`, `failed` 중 UI가 표시할 결과 상태.
- `error_message`: 거부 또는 실패 사유. 민감 정보는 D03 기준으로 마스킹한다.
- `audit_event`: action, actor, target, outcome, reason, timestamp, request id 후보를 담는 감사 이벤트. 이번 Milestone에서는 Core in-memory/event surface 또는 테스트 가능한 adapter 경계까지만 다룬다.
- `ui_state`: disabled, submitting, succeeded, deferred, failed, empty/error state 중 operator가 관찰할 상태.
- UI 노출:
- `action`: 운영 action 이름.
- `actor_reference`: operator/admin 또는 runner actor의 표시 가능한 참조값.
- `target_id`: runner, job, execution 등 대상 식별자.
- `outcome`: `accepted`, `rejected`, `deferred`, `failed` 등 결과.
- `timestamp`: action 발생 또는 기록 시각.
- `reason`: operator가 입력한 선택적 사유.
- 금지:
- auth token, enrollment token, credential 원문, private release URL credential을 SDD, audit event, UI text, 테스트 fixture에 남기지 않는다.
- execution owner mismatch, terminal state rejection, unknown runner 같은 기존 Core 보호 조건을 우회하지 않는다.
- 원격 빌드 로그 원문, raw log line, artifact 파일 내용, artifact full path를 audit event에 복제하지 않는다.
- 사용자 로그인 히스토리 전체나 production identity provider event를 이번 Milestone의 audit source of truth로 삼지 않는다.
- durable audit persistence 구현을 기본 완료 기준으로 삼지 않는다.
- artifact path, log line, token reference, credential-like error payload 같은 민감 후보를 감사 UI에 그대로 노출하지 않는다.
## Acceptance Scenarios
| ID | Milestone Task | Given | When | Then |
|----|----------------|-------|------|------|
| S01 | `security-sdd` | SDD가 필요한 운영 보안·감사 Milestone이고 D01-D03이 미확정이다. | SDD와 사용자 리뷰가 작성된다. | SDD는 잠금 상태로 남고, 사용자 결정 항목은 `USER_REVIEW.md`에만 분리된다. |
| S02 | `security-sdd` | 사용자 결정 D01-D03이 해결되었다. | SDD를 갱신하고 gate를 확인한다. | SDD 상태는 `[승인됨]`, SDD 잠금은 `해제` 후보가 되며, Milestone 잠금 해제 조건을 검증할 수 있다. |
| S03 | `auth-scope` | 최소 인증 모델과 역할 범위가 확정되어 있다. | Core write endpoint와 Flutter action surface의 권한 허용/거부 흐름을 계획한다. | 기능 Task는 actor, role, unauthorized/forbidden UX, disabled/error 상태의 검증 기준을 포함한다. |
| S04 | `audit-scope` | 감사 저장 책임과 UI 노출/마스킹 기준이 확정되어 있다. | action outcome을 감사 이벤트 후보와 UI 표면으로 계획한다. | 기능 Task는 event field, in-memory/event surface boundary, masking rule, durable persistence 후속 분리 여부의 검증 기준을 포함한다. |
## Evidence Map
| Scenario | Required Evidence | `agent-task` 연결 | `Spec Completion` 기대 |
|----------|-------------------|------------------|---------------------------|
| S01 | `SDD.md` 생성, `USER_REVIEW.md` 생성, Milestone Task id 매핑 확인 | `agent-task/m-control-plane-security-audit-surface/...` | `security-sdd`: SDD 초안과 사용자 리뷰 파일이 생성되어 잠금 사유가 명확하다. |
| S02 | 해결된 `user_review_N.log`, 갱신된 SDD 상태/잠금, check-gate 결과 | `agent-task/m-control-plane-security-audit-surface/...` | `security-sdd`: D01-D03 답변이 SDD에 반영되고 gate가 pass 또는 남은 차단 항목을 보고한다. |
| S03 | Core handler/route 테스트 또는 adapter/widget 테스트, 권한 실패 UI 확인, 민감 정보 미노출 검색 | `agent-task/m-control-plane-security-audit-surface/...` | `auth-scope`: 권한 허용/거부 및 disabled/error 상태가 코드와 테스트 근거로 검증된다. |
| S04 | audit event field 검증, storage boundary 검증, UI masking/widget 테스트 또는 문자열 검색 | `agent-task/m-control-plane-security-audit-surface/...` | `audit-scope`: 감사 이벤트 범위와 마스킹 기준이 구현·테스트·검색 근거로 검증된다. |
## Cross-repo Dependencies
- 없음
<!-- 필요한 경우:
- `<project>:agent-roadmap/phase/<phase>/milestones/<milestone>.md`
- `.agent-roadmap-sync/locks.yaml` entry: `<id>`
-->
## Drift Check
- [x] Milestone 기능 Task와 Acceptance Scenario가 일치한다.
- [x] Evidence Map이 plan/code-review/complete.log에서 검증 가능하다.
- [x] agent-contract를 쓰는 경우 SDD에 계약 원문을 복제하지 않았다.
- [x] 사용자 리뷰가 필요한 항목은 `USER_REVIEW.md`에 분리했고 해결 로그로 이동한다.
## 사용자 리뷰 이력
- D01: 추천안 승인. 이번 Milestone은 production identity provider를 붙이지 않고, operator/admin 단일 역할과 `OtoConsoleConfig.authTokenReference` 기반의 표시 가능한 token reference만 기준으로 설계한다.
- D02: 추천안 승인. 감사 로그는 원격 빌드 로그 원문이나 production 로그인 히스토리가 아니라, Control Plane write action의 actor/action/target/outcome/timestamp/optional reason metadata로 한정하고 Core in-memory/event surface 또는 테스트 가능한 adapter 경계까지만 이번 Milestone 범위로 둔다.
- D03: 추천안 승인. 감사 UI는 운영 판단에 필요한 action, actor reference, target id, outcome, timestamp, optional reason만 보여주고, 로그/아티팩트/토큰류 원문과 민감 error payload는 복제 노출하지 않는다.
## 작업 컨텍스트
- 표준선: 기존 Core HTTP route와 Flutter action result contract를 우선 source of truth로 삼고, API/schema 확장은 D01-D03 결정 이후 필요한 최소 범위로만 분리한다.
- 표준선: `OtoConsoleConfig.authTokenReference`는 표시 가능한 reference로만 취급하고 token 원문은 UI, 감사 이벤트, 문서, 테스트 fixture에 남기지 않는다.
- 표준선: 원격 빌드 로그 원문과 artifact 내용은 기존 execution log/artifact 도메인의 데이터이며, audit event에는 append/report 같은 action metadata만 남긴다.
- 표준선: 감사 UI는 운영 판단용 metadata만 보여주며, credential-like substring과 민감 후보 payload는 마스킹 또는 비노출 대상으로 둔다.
- 후속 SDD: durable audit storage가 필요해지면 별도 persistence/schema SDD 또는 후속 Milestone으로 분리한다.

View file

@ -0,0 +1,62 @@
# SDD User Review
## 상태
해결됨
## 검토 대상
- SDD: `agent-roadmap/sdd/control-plane-product-surface/control-plane-security-audit-surface/SDD.md`
- Milestone: `agent-roadmap/phase/control-plane-product-surface/milestones/control-plane-security-audit-surface.md`
## 사용자 결정 항목
### [D01] 최소 인증/권한 범위
- 상태: 해결됨
- 결정 필요: 이 Phase에서 Control Plane write action에 적용할 최소 인증 모델과 권한 역할을 어디까지 포함할지 결정한다.
- 추천안: 이번 Milestone은 production identity provider를 붙이지 않고, operator/admin 단일 역할과 `OtoConsoleConfig.authTokenReference` 기반의 표시 가능한 token reference만 기준으로 설계한다. 실제 token 원문 저장·회전·조직/테넌트 권한은 후속 Milestone으로 분리한다.
- 대안: viewer/operator/admin 역할을 지금부터 분리하거나, production identity provider 연동을 현재 Phase 범위에 포함한다.
- 영향: 권한 실패 상태, UI disabled/error 상태, Core handler hook 위치, 테스트 범위, 후속 API/schema 필요 여부가 달라진다.
- 적용 위치:
- SDD: `Source of Truth`, `State Machine`, `Interface Contract`, `Acceptance Scenarios`
- Milestone: `auth-scope`, `구현 잠금`
### [D02] 감사 로그 저장 책임
- 상태: 해결됨
- 결정 필요: 감사 로그의 source of truth를 Core in-memory/event surface로 둘지, durable persistence를 현재 Milestone 범위에 포함할지 결정한다.
- 추천안: 이번 Milestone은 Core write action 결과에서 audit event 후보를 만들고 in-memory/event surface 또는 테스트 가능한 adapter 경계까지만 고정한다. durable persistence는 후속 Milestone으로 분리한다.
- 대안: file/DB 기반 durable audit storage와 retention 정책을 현재 Milestone 구현 범위로 포함한다.
- 영향: DB/schema 또는 storage contract 필요 여부, retention/삭제 정책, 완료 evidence, 운영 비용과 범위가 달라진다.
- 적용 위치:
- SDD: `Problem / Non-goals`, `Source of Truth`, `State Machine`, `Interface Contract`, `Evidence Map`
- Milestone: `audit-scope`, `범위 제외`, `구현 잠금`
### [D03] 감사 이벤트 노출과 마스킹 기준
- 상태: 해결됨
- 결정 필요: UI에 노출할 감사 이벤트 필드와 민감 정보 마스킹 기준을 결정한다.
- 추천안: UI에는 action, actor reference, target id, outcome, timestamp, optional reason만 노출하고 token, credential, private URL credential, raw log line, full artifact path는 노출하지 않는다. 에러 메시지는 기존 Core response를 사용하되 credential-like substring은 마스킹 대상으로 둔다.
- 대안: artifact path나 log summary 일부를 운영 편의 필드로 노출하거나, 감사 UI를 이번 Milestone에서 만들지 않고 plan/evidence만 남긴다.
- 영향: UI 표시 범위, 테스트 fixture, 문자열 검색 검증, 보안 리뷰 기준이 달라진다.
- 적용 위치:
- SDD: `Interface Contract`, `Acceptance Scenarios`, `Evidence Map`
- Milestone: `audit-scope`, `security-sdd`, `구현 잠금`
## 승인 항목
- [x] 위 결정 항목을 승인했다.
- [x] SDD 잠금 해제를 승인했다.
## 답변 기록
- D01: 추천안 승인. 이번 Milestone은 production identity provider를 붙이지 않고, operator/admin 단일 역할과 `OtoConsoleConfig.authTokenReference` 기반의 표시 가능한 token reference만 기준으로 설계한다.
- D02: 추천안 승인. 감사 로그는 원격 빌드 로그 원문이나 production 로그인 히스토리가 아니라, Control Plane write action의 actor/action/target/outcome/timestamp/optional reason metadata로 한정하고 Core in-memory/event surface 또는 테스트 가능한 adapter 경계까지만 이번 Milestone 범위로 둔다.
- D03: 추천안 승인. 감사 UI는 운영 판단에 필요한 action, actor reference, target id, outcome, timestamp, optional reason만 보여주고, 로그/아티팩트/토큰류 원문과 민감 error payload는 복제 노출하지 않는다.
## 해결 조건
- 모든 사용자 결정 항목의 답변이 SDD에 반영되어 있다.
- `USER_REVIEW.md`가 `user_review_N.log`로 이동되어 있다.
- 남은 잠금 항목이 없으면 SDD 상태가 `[승인됨]`이고 `SDD 잠금` 상태가 `해제`다.

View file

@ -98,6 +98,7 @@ class OtoHttpCoreConnectionClient implements OtoCoreConnectionClient {
}
enum OtoCoreReadState { loading, empty, data, error }
enum OtoCoreWriteState { idle, submitting, succeeded, deferred, failed }
class OtoCoreWriteResult<T> {
@ -119,25 +120,28 @@ class OtoCoreWriteResult<T> {
const OtoCoreWriteResult.submitting({String message = 'Submitting'})
: this._(state: OtoCoreWriteState.submitting, message: message);
const OtoCoreWriteResult.succeeded(T data, {String message = 'Created', int? statusCode})
: this._(
state: OtoCoreWriteState.succeeded,
data: data,
message: message,
statusCode: statusCode,
);
const OtoCoreWriteResult.deferred({String message = 'Deferred', int? statusCode})
: this._(
state: OtoCoreWriteState.deferred,
message: message,
statusCode: statusCode,
);
const OtoCoreWriteResult.failed({
required String message,
const OtoCoreWriteResult.succeeded(
T data, {
String message = 'Created',
int? statusCode,
}) : this._(
state: OtoCoreWriteState.succeeded,
data: data,
message: message,
statusCode: statusCode,
);
const OtoCoreWriteResult.deferred({
String message = 'Deferred',
int? statusCode,
}) : this._(
state: OtoCoreWriteState.deferred,
message: message,
statusCode: statusCode,
);
const OtoCoreWriteResult.failed({required String message, int? statusCode})
: this._(
state: OtoCoreWriteState.failed,
message: message,
statusCode: statusCode,
@ -275,9 +279,7 @@ class OtoHttpCoreWriteClient implements OtoCoreWriteClient {
try {
uri = _endpointUri(config.serverHttpUrl, '/api/v1/jobs');
} on FormatException catch (_) {
return const OtoCoreWriteResult.failed(
message: 'Invalid server URL',
);
return const OtoCoreWriteResult.failed(message: 'Invalid server URL');
}
try {
@ -334,7 +336,8 @@ class OtoHttpCoreWriteClient implements OtoCoreWriteClient {
}
return OtoCoreWriteResult.failed(
message: 'Unexpected HTTP ${response.statusCode}: ${response.body.trim()}',
message:
'Unexpected HTTP ${response.statusCode}: ${response.body.trim()}',
statusCode: response.statusCode,
);
} on TimeoutException {
@ -351,15 +354,17 @@ class OtoHttpCoreWriteClient implements OtoCoreWriteClient {
OtoConsoleConfig config,
OtoExecutionCancelDraft draft,
) async {
final runnerID = draft.runnerID ?? '';
final runnerID = (draft.runnerID ?? '').trim();
final execID = draft.executionID;
Uri uri;
if (runnerID.isEmpty) {
return const OtoCoreWriteResult.failed(message: 'Runner ID is required');
}
try {
if (runnerID.isNotEmpty) {
uri = _endpointUri(config.serverHttpUrl, '/api/v1/runners/$runnerID/executions/$execID/cancel');
} else {
uri = _endpointUri(config.serverHttpUrl, '/api/v1/executions/$execID/cancel');
}
uri = _endpointUri(
config.serverHttpUrl,
'/api/v1/runners/$runnerID/executions/$execID/cancel',
);
} on FormatException catch (_) {
return const OtoCoreWriteResult.failed(message: 'Invalid server URL');
}
@ -388,15 +393,17 @@ class OtoHttpCoreWriteClient implements OtoCoreWriteClient {
OtoConsoleConfig config,
OtoExecutionReportDraft draft,
) async {
final runnerID = draft.runnerID ?? '';
final runnerID = (draft.runnerID ?? '').trim();
final execID = draft.executionID;
Uri uri;
if (runnerID.isEmpty) {
return const OtoCoreWriteResult.failed(message: 'Runner ID is required');
}
try {
if (runnerID.isNotEmpty) {
uri = _endpointUri(config.serverHttpUrl, '/api/v1/runners/$runnerID/executions/$execID/report');
} else {
uri = _endpointUri(config.serverHttpUrl, '/api/v1/executions/$execID/report');
}
uri = _endpointUri(
config.serverHttpUrl,
'/api/v1/runners/$runnerID/executions/$execID/report',
);
} on FormatException catch (_) {
return const OtoCoreWriteResult.failed(message: 'Invalid server URL');
}
@ -430,9 +437,15 @@ class OtoHttpCoreWriteClient implements OtoCoreWriteClient {
Uri uri;
try {
if (runnerID.isNotEmpty) {
uri = _endpointUri(config.serverHttpUrl, '/api/v1/runners/$runnerID/executions/$execID/logs');
uri = _endpointUri(
config.serverHttpUrl,
'/api/v1/runners/$runnerID/executions/$execID/logs',
);
} else {
uri = _endpointUri(config.serverHttpUrl, '/api/v1/executions/$execID/logs');
uri = _endpointUri(
config.serverHttpUrl,
'/api/v1/executions/$execID/logs',
);
}
} on FormatException catch (_) {
return const OtoCoreWriteResult.failed(message: 'Invalid server URL');
@ -467,9 +480,15 @@ class OtoHttpCoreWriteClient implements OtoCoreWriteClient {
Uri uri;
try {
if (runnerID.isNotEmpty) {
uri = _endpointUri(config.serverHttpUrl, '/api/v1/runners/$runnerID/executions/$execID/artifacts');
uri = _endpointUri(
config.serverHttpUrl,
'/api/v1/runners/$runnerID/executions/$execID/artifacts',
);
} else {
uri = _endpointUri(config.serverHttpUrl, '/api/v1/executions/$execID/artifacts');
uri = _endpointUri(
config.serverHttpUrl,
'/api/v1/executions/$execID/artifacts',
);
}
} on FormatException catch (_) {
return const OtoCoreWriteResult.failed(message: 'Invalid server URL');
@ -486,7 +505,9 @@ class OtoHttpCoreWriteClient implements OtoCoreWriteClient {
return _parseExecutionActionResponse(response, 'artifact');
} on TimeoutException {
return const OtoCoreWriteResult.failed(message: 'Artifact report timed out');
return const OtoCoreWriteResult.failed(
message: 'Artifact report timed out',
);
} on FormatException catch (error) {
return OtoCoreWriteResult.failed(message: error.message);
} on Exception catch (error) {
@ -571,7 +592,8 @@ class OtoHttpCoreWriteClient implements OtoCoreWriteClient {
}
return OtoCoreWriteResult.failed(
message: 'Unexpected HTTP ${response.statusCode}: ${response.body.trim()}',
message:
'Unexpected HTTP ${response.statusCode}: ${response.body.trim()}',
statusCode: response.statusCode,
);
} on TimeoutException {
@ -596,14 +618,26 @@ class OtoHttpCoreWriteClient implements OtoCoreWriteClient {
final body = response.body.trim();
if (response.statusCode >= 400) {
final message = _extractErrorMessage(response.body, 'HTTP ${response.statusCode}');
final message = _extractErrorMessage(
response.body,
'HTTP ${response.statusCode}',
);
if (response.statusCode == 400) {
return OtoCoreWriteResult.failed(message: message, statusCode: response.statusCode);
return OtoCoreWriteResult.failed(
message: message,
statusCode: response.statusCode,
);
}
if (response.statusCode == 404) {
return OtoCoreWriteResult.failed(message: message, statusCode: response.statusCode);
return OtoCoreWriteResult.failed(
message: message,
statusCode: response.statusCode,
);
}
return OtoCoreWriteResult.failed(message: message, statusCode: response.statusCode);
return OtoCoreWriteResult.failed(
message: message,
statusCode: response.statusCode,
);
}
// Accept 2xx (200 OK, 201 Created)
@ -617,7 +651,8 @@ class OtoHttpCoreWriteClient implements OtoCoreWriteClient {
}
// Check for `success` field (cancel response) or `accepted` field (report/log/artifact)
final isSuccess = decoded['success'] == true || decoded['accepted'] == true;
final isSuccess =
decoded['success'] == true || decoded['accepted'] == true;
final errorMessage = _string(decoded, 'error_message');
if (isSuccess && errorMessage.isEmpty) {
@ -667,7 +702,11 @@ class OtoHttpCoreWriteClient implements OtoCoreWriteClient {
final decoded = jsonDecode(body);
if (decoded is Map<String, dynamic>) {
// Execution action responses use error_message field
var message = decoded['error_message'] ?? decoded['message'] ?? decoded['error'] ?? '';
var message =
decoded['error_message'] ??
decoded['message'] ??
decoded['error'] ??
'';
if (message is String && message.isNotEmpty) {
return message;
}

File diff suppressed because it is too large Load diff