From b0dfc42966a6181de80f4f4858347fc2ab68d6fe Mon Sep 17 00:00:00 2001 From: Justin Ko Date: Sun, 28 Jun 2026 00:06:48 +0900 Subject: [PATCH] =?UTF-8?q?docs(dcy-010):=20confirm-discovery=20=E2=80=94?= =?UTF-8?q?=20daemon-scoped=20=EB=B8=8C=EB=A1=9C=EC=BB=A4=20=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D=EC=A0=95=EB=B3=B4=20provisioning=20(PK=20(daemon=5Fid?= =?UTF-8?q?,broker)=C2=B7/v1/user=5Finfo=20=ED=99=95=EC=9E=A5=C2=B71?= =?UTF-8?q?=EB=8D=B0=EB=AA=AC1=EA=B3=84=EC=A2=8C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01VHtM5Ws1HKCKQ4JGi6GRb5 --- ...n-scoped-broker-credential-provisioning.md | 194 ++++++++++++++++++ docs/discovery/index.md | 1 + 2 files changed, 195 insertions(+) create mode 100644 docs/discovery/dcy-010_20260627_daemon-scoped-broker-credential-provisioning.md diff --git a/docs/discovery/dcy-010_20260627_daemon-scoped-broker-credential-provisioning.md b/docs/discovery/dcy-010_20260627_daemon-scoped-broker-credential-provisioning.md new file mode 100644 index 0000000..018dd70 --- /dev/null +++ b/docs/discovery/dcy-010_20260627_daemon-scoped-broker-credential-provisioning.md @@ -0,0 +1,194 @@ +# 0. 문서 상태 +이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다. + +- 상태: confirmed +- 문서 ID: dcy-010_20260627_daemon-scoped-broker-credential-provisioning +- 이슈명: daemon-scoped 브로커(KIS) 인증정보 소유·전달 — 다중 인스턴스 trex-core가 daemon별 다른 계좌로 거래 (교차 발원 trex-core 중장기 멀티 인스턴스) +- 작성 시각(KST): 2026-06-27 +- 마지막 갱신 시각(KST): 2026-06-28 +- 대체됨: 없음 +- 후속 Discovery 참조: trex-core 소비자측 day-start fetch + fail-safe 로드 = 별도 trex-core Discovery로 분리 **확정**(2026-06-28). trex-core repo에 신규 Discovery 생성 예정(미생성). +- 생성된 REQ 참조: 없음 + +# 1. 계획 상태 요약 +현재 Discovery가 어떤 배경에서 작성되고 있는지, 연계 문서가 있는지, 지금 시점의 해석이 무엇인지 짧게 요약한다. + +- 연계 문서 현황: + - 관련 점수 파일: 미생성 + - 관련 TODO/메모: TREX 워크스페이스 M3 메인 목표(중장기 trex-core 인스턴스 추가). GitHub org 보드 fourblocks2020/trex #7. 교차 발원 = trex-core 멀티 인스턴스(instance_key) + dcy-008(WAC cost-basis seed) +- 해석: + - 사장님이 "다중 인스턴스 trex-core를 완벽히 지원하려면 KIS API 인증정보를 portfolio-hub에서 받아야 하고, 그 인증정보는 universe와 동일하게 daemon_id로 매칭되어야 한다"고 방향을 확정했다. 현재 portfolio-hub는 broker 인증정보를 `user_id` 단위로만 소유해(`broker_credentials` PK=(user_id,broker)) 같은 user의 두 데몬에 서로 다른 계좌를 줄 수 없다. + - 이번 Discovery에서 판단할 핵심: 브로커 인증정보를 daemon_id 차원으로 확장(스키마 PK 변경 + daemon-scoped 전달 엔드포인트)하고, "1 데몬 = 1 계좌" 불변식 하에 응답을 단일 `broker`로 정규화하는 범위·리스크·마이그레이션을 확정한다. + +# 2. 요구사항 판정 결과 +이번 요청이 기존 요구사항과 비교해 신규인지, 유사한지, 중복인지 판단하고 그 근거를 남긴다. + +- 판정: 신규 +- 근거 + - 기존 `broker_credentials` + `/v1/user_info`(dcy-004 토큰 정비 계열)는 user-scoped 인증정보 소유·전달을 다뤘으나, daemon_id 차원의 스코프·1:1 불변식·daemon 부트스트랩 전달은 정의된 바 없다. + - 변경이 테이블 PK와 전달 엔드포인트 응답 형태(brokers 리스트→단일 broker)를 바꾸는 구조 변경이라 기존 Discovery의 미세 조정이 아닌 독립 주기로 추적한다. + +# 3. 문제점의 요약 +문제의 현재 상태, 기대 상태, 관련 이해관계자, 제약사항을 정리한다. + +- 현재 상태 + - `broker_credentials` 테이블 PK=(`user_id`, `broker`) — 인증정보가 **user 단위**. 컬럼: user_id, broker, account_ref, encrypted_payload(Fernet 암호화 JSON `{app_key, app_secret}`), credential_version, last_rotated_at. (`migrations/versions/20260502_0014_v1_api_schema.py`, `app/domain/models.py:178`) + - 전달: `GET /v1/user_info` — **service token 전용**(token이 `daemon_id`/`runtime_id` 바인딩), 토큰의 `user_id`로 조회 → 서버에서 Fernet 복호화 → `brokers: list[BrokerItem]`(`credential_payload`, `account_ref`, `credential_version`) + `trading_enabled`(user.is_active) 반환. (`app/api/v1_user_info.py`) + - 반면 **universe(포트폴리오)는 이미 daemon-scoped** — `Portfolio.managed_by_daemon_id`, `/v1/portfolios`가 service token의 `auth.daemon_id == managed_by_daemon_id` 일치로 필터·인가. (`app/api/v1_portfolios.py:254,260,554`) + - 결과 비대칭: 토큰은 daemon_id를 들고 universe는 daemon으로 거르는데, **인증정보만 user로 걸러** 같은 user의 두 데몬이 같은 계좌를 공유하게 된다. + - trex-core 측은 현재 KIS를 `TREX_KIS_BROKER_*` env로만 주입(`trex-core/runtime/config.py:257`)하며 portfolio-hub에서 인증정보를 받지 않는다. 멀티 인스턴스 토대(instance_key, longterm.example.toml)는 존재. +- 기대 상태 + - 한 trex-core 데몬 인스턴스가 자기 service token(daemon_id 바인딩)으로 부트스트랩 시 **그 daemon에 매칭된 단일 KIS 계좌 인증정보**를 받는다. + - 같은 user가 단기·중장기 데몬을 동시에 소유해도 **데몬마다 다른 KIS 계좌**로 거래할 수 있다. + - 인증정보는 universe와 동일하게 daemon_id로 인가·스코프되어, 계좌-인스턴스 매칭이 단일 출처(portfolio-hub)에서 결정된다. +- 이해관계자 + - 사장님(제품 방향·계좌/인증 정책 결정·결재) + - 팀장(Discovery·거버넌스·검수) + - portfolio-hub-dev(스키마·엔드포인트·CLI 구현), trex-core-dev(소비자 어댑터 — 별도 Discovery) + - 운영자(계좌 인증정보 등록/회전, 데몬-계좌 바인딩 관리) +- 제약사항 + - KIS app_secret = 시스템 최민감 시크릿(실계좌=실자금). 복호화 평문이 응답 본문으로 네트워크 전송됨 → 내부망/TLS·service-token 게이트 유지 필수. + - portfolio-hub는 M3에서 trex-web 프론트로 노출될 서비스 — 인증정보 경로를 web 노출 표면과 격리 유지(service-token 전용). + - PK 변경은 기존 데이터·제약·인덱스에 영향(마이그레이션 비가역성 주의). + +## 주요 사용자 시나리오 +- 운영자가 단기 전략 데몬 A와 중장기 전략 데몬 B를 같은 user 소유로 등록하고, 각각 **다른 KIS 계좌** 인증정보를 portfolio-hub에 등록한다. A·B는 각자 자기 계좌로 거래한다. +- trex-core 중장기 인스턴스가 그날 시작(아침 스냅샷) 시 service token으로 부트스트랩을 호출해 자기 daemon의 universe + KIS 인증정보를 한 번에 받아 그날 거래를 개시한다. +- 운영자가 데몬 B의 KIS app_secret을 회전(rotate)하면, B만 새 버전을 받고 A는 영향받지 않는다. + +# 4. 이번에 정의할 변경 +이번 Discovery에서 다루는 핵심 변경 항목. + +- (C1) `broker_credentials`에 **`daemon_id` 차원 도입** — 인증정보를 daemon-scope로 소유. PK를 `(user_id, broker)` → daemon 포함 형태로 변경. +- (C2) **daemon-scoped 전달** — service token의 `auth.daemon_id`로 해당 데몬의 인증정보만 복호화·반환. "1 데몬 = 1 계좌" 불변식 하에 응답을 **단일 `broker`**(리스트 아님)로 정규화. +- (C3) **인증정보 관리 경로(CLI/관리 API)에 daemon_id 바인딩 추가** — 등록·회전·삭제 시 데몬-계좌 매칭 지정. + +## 영향 범위 +- 코드 영향: + - `app/domain/models.py`(BrokerCredential), `app/repositories/*`(upsert/list/delete 시그니처 + 쿼리 daemon_id 필터), `migrations/versions/*`(PK·컬럼 마이그레이션) + - `app/api/v1_user_info.py`(기존 `/v1/user_info` daemon-scoped 확장 — 결정 2026-06-28, 신규 엔드포인트 없음), `app/schemas/v1.py`(UserInfoResponse: brokers 리스트→단일 broker) + - `app/cli/commands/credential.py`(`--daemon-id` 추가) + - **교차 서비스(별도 Discovery)**: trex-core — KISConfig를 env-only → portfolio-hub fetch 소스에서 구성하는 소비자 어댑터 + 그날-시작 fail-safe 로드 게이트(`trex-core/runtime/config.py`, daemon 부트스트랩) +- 문서 영향: + - `docs/srs/`(신규 REQ), `docs/discovery/index.md`(본 문서 등록) + - baseline 런타임 흐름에 "daemon 부트스트랩 시 인증정보 전달" 추가 검토(§ runtime-flows 갱신은 NFR/후속에서 판단) +- 운영 영향: + - 운영자 인증정보 등록 절차에 daemon_id 지정 추가. 기존 user-scoped 인증정보 마이그레이션(데몬 매핑) 1회 필요. + - `CREDENTIAL_ENCRYPTION_KEY` 관리·회전 정책(기존 의존 유지). + +# 5. 요구사항 목록 + +## 기능 요구사항 +- FR-1: `broker_credentials`가 `daemon_id`로 인증정보를 스코프한다. 한 (daemon, broker) 조합당 활성 인증정보는 정확히 1건이다. **PK = `(daemon_id, broker)`, daemon-scoped 강제(user-기본 폴백 없음 — 공유계좌 footgun 차단). 결정 2026-06-28.** 마이그레이션은 nullable `daemon_id` 컬럼 추가 → 백필(데몬 매핑) → PK 전환의 단계적 경로. +- FR-2: **기존 `GET /v1/user_info`를 daemon-scoped로 확장**한다(신규 엔드포인트 미신설 — confirm 2026-06-28). service token 호출 시 토큰의 `daemon_id`에 매칭된 **단일 broker 인증정보**(account_ref + 복호화된 credential_payload + credential_version)와 `trading_enabled`를 반환한다. 토큰에 daemon_id가 없으면 거부(403)한다(universe 인가와 동일 규율). 1데몬1계좌 불변식(A-2 confirmed)상 응답은 리스트가 아닌 단일 broker로 정규화한다. 기존 소비자는 trex-core `hub_read.whoami()` 하나뿐이며 `daemon_id`만 읽으므로 broker 필드 reshape는 무영향(안전). +- FR-3: 인증정보 등록/회전/삭제 경로(CLI 및/또는 관리 API)가 `daemon_id`를 받아 데몬-계좌 매칭을 지정한다. +- FR-4: 기존 user-scoped 인증정보를 daemon-scoped로 이전하는 마이그레이션 경로를 제공한다(데이터 손실 없이). + +## 비기능 요구사항 +- NFR-1 (보안): 복호화된 app_secret은 service-token 전용·내부망/TLS 경로로만 전달된다. trex-web 등 user 토큰 경로로는 접근 불가해야 한다. at-rest는 기존 Fernet 암호화 유지. +- NFR-2 (일관성): 인증정보 스코프(daemon_id)가 universe 스코프(`managed_by_daemon_id`)와 동일한 인가 규율을 따른다(불일치/누락 → 403). +- NFR-3 (가용성/degrade): 인증정보 조회·복호화 실패가 부트스트랩 응답의 필수 필드(daemon_id, trading_enabled)를 크래시시키지 않는다(기존 graceful degrade 원칙 유지). 단, 인증정보 부재는 그날 거래 개시 차단 신호로 전달된다. + +## 범위 경계 +- In Scope + - portfolio-hub: daemon_id 차원 인증정보 소유(PK 변경) + daemon-scoped 단일 broker 전달 + 관리 경로 daemon_id 바인딩 + 마이그레이션. + - "1 데몬 = 1 계좌" 불변식 확정 및 응답 단일화. +- Out of Scope + - trex-core 소비자 어댑터(KISConfig fetch) + 그날-시작 fail-safe 로드 게이트 — **별도 trex-core Discovery**로 분리(본 문서는 계약·전달 형태만 확정). + - {{DECIDE_SCOPE_BOOTSTRAP_UNIFY:부트스트랩 응답에 universe까지 한 엔드포인트로 묶을지(단일 부트스트랩) vs 인증정보만 별도 경로로 둘지 — 사장님/팀장 확정 필요}} + - 시크릿 매니저(Vault 등) 외부화 — 현 단계는 portfolio-hub 내부 처리 유지(가정 A-3). + +# 6. 리스크/가정 목록 + +## 리스크 +- R-1 (High): `broker_credentials` PK 변경 마이그레이션이 기존 데이터·제약을 깨거나 롤백이 어려움. + - 완화 방안: nullable `daemon_id` 컬럼 추가 → 백필(데몬 매핑) → 새 PK/유니크 제약 전환의 단계적 마이그레이션. 사전 백업·드라이런·롤백 스크립트. 비가역 단계는 사장님 결재. +- R-2 (High): 최민감 시크릿(app_secret) 평문이 네트워크 전송되며, portfolio-hub가 web 노출 서비스라 표면이 큼. + - 완화 방안: service-token 전용 + daemon_id 인가 + 내부망/TLS 한정. web의 user 토큰 경로와 라우트/권한 분리. 감사 로깅. +- R-3 (Medium): `/v1/user_info` 응답 형태(brokers 리스트→단일)의 파괴적 변경이 기존 소비자를 깸. + - 완화 방안: 기존 소비자 인벤토리 확인 후, 신규 daemon-scoped 경로 신설(기존 user-level 리스트는 admin/UI용으로 유지) 또는 버전드 전환. + +## 가정 +- A-1: portfolio-hub가 universe·전략·인증정보의 단일 진실원천이다. (사장님 확정) +- A-2: "1 데몬(trex-core 인스턴스) = 1 브로커 계좌" 불변식이 성립한다. trex-core `KISConfig`가 현재 단일인 점과 정합. (**사장님 confirmed 2026-06-28** → 단일 `broker` 응답 근거) +- A-3: 현 단계는 portfolio-hub 내부 인증정보 소유(기존 Fernet + `CREDENTIAL_ENCRYPTION_KEY`)를 유지하고 외부 시크릿 매니저로 가지 않는다. +- A-4: 데몬-계좌 매칭은 service token의 daemon_id 바인딩(`AuthToken.daemon_id`)을 통해 결정된다. + +## 오픈 질문 +- OQ-1: PK 전환 형태 + 폴백 정책. + - 상태: **Answered (2026-06-28)** + - 결정: **PK = `(daemon_id, broker)`, daemon-scoped 강제(폴백 없음)**. 마이그레이션 = nullable `daemon_id` 컬럼 추가 → 백필 → PK 전환 단계적. 본문 FR-1·R-1에 반영. + - 반영 위치: §5 FR-1, §6 R-1 완화 방안. +- OQ-2: 전달 형태 — 기존 `/v1/user_info` reshape vs daemon-scoped 신규 엔드포인트. + - 상태: **Answered (2026-06-28)** + - 결정: **기존 `/v1/user_info`를 daemon-scoped로 확장**(신규 엔드포인트 미신설, 사장님). 소비자 인벤토리 조사 결과 유일 소비자=trex-core `adapters/portfolio/hub_read.py::whoami()`이며 `daemon_id`만 읽음(`brokers` 미참조) → 단일 broker reshape 무영향(안전). 본문 FR-2에 반영. + - 반영 위치: §4 영향범위, §5 FR-2, §7 S-1/S-2. +- OQ-3: 부트스트랩 통합 범위 — universe + 인증정보 + trading_enabled를 단일 부트스트랩 응답으로 묶을지. + - 상태: Deferred + - 처리 방안: trex-core 소비자 Discovery와 함께 결정(전달 계약 공동 설계). 본 문서는 인증정보 전달 형태만 확정. + - 종료 조건: trex-core 소비자 Discovery 착수 시점. +- OQ-4: 그날-시작 fail-safe 로드 의미론(진입 게이트 vs 청산 보존, 장중 무중단)의 trex-core측 구현 책임 분배. + - 상태: Deferred + - 처리 방안: 별도 trex-core Discovery로 인계(아래 §10 참조). 본 문서엔 전달측 가정으로만 기록. + - 종료 조건: trex-core 소비자 Discovery 생성. + +# 7. 초기 성공 기준 +- S-1: 같은 user가 소유한 두 데몬이 각각 다른 KIS `account_ref`/`app_key`를 부트스트랩으로 수신함을 검증할 수 있다. +- S-2: service token에 daemon_id가 없거나 인증정보가 daemon에 매칭되지 않으면 전달이 403/차단됨이 테스트로 확인된다. +- S-3: 기존 user-scoped 인증정보가 마이그레이션 후 손실 없이 daemon-scoped로 조회된다. + +## 측정 방식 및 데이터 출처 +- S-1 측정 방식: 두 데몬용 service token으로 부트스트랩 호출 → 응답 account_ref/credential_payload 상이 확인 / 데이터 출처: 통합테스트 + `broker_credentials` 행 +- S-2 측정 방식: daemon_id 누락/불일치 토큰 요청의 403 단위·통합 테스트 / 데이터 출처: 엔드포인트 테스트 +- S-3 측정 방식: 마이그레이션 전후 인증정보 복호화 결과 동등성 비교 / 데이터 출처: 마이그레이션 드라이런 로그 + 테스트 + +# 8. REQ로 넘기기 전 확인 체크 +- [x] 범위(In Scope/Out of Scope) 확정 + - 기준: trex-core 소비자측 분리 확정(별도 Discovery), 부트스트랩 통합(OQ-3) Deferred 처리 +- [x] 성공 지표의 측정 방식/데이터 출처 확정 + - 기준: S-1~S-3 측정 가능성 검토 완료 +- [x] High 영향 리스크 우선순위 및 해소 책임자 지정 + - 기준: R-1(마이그레이션 — 단계적 PK 전환·롤백, portfolio-hub-dev)·R-2(시크릿 표면 — service-token+내부망) 완화안 확정 +- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인 + - 기준: OQ-1·OQ-2 Answered, OQ-3·OQ-4 Deferred 처리 완료 +- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정 + - 기준: 사장님+팀장 confirm-discovery (DECIDE/CONFIRM 전건 해소) + +# 9. 파일 처리 결과 +- 처리 결과: 생성 +- 생성 경로: docs/discovery/dcy-010_20260627_daemon-scoped-broker-credential-provisioning.md +- 참조 문서: + - docs/discovery/dcy-004_20260520_token-issuance-and-lifecycle-overhaul.md (service token·daemon 바인딩) + - docs/discovery/dcy-008_20260625_current-holding-wac-cost-basis-read.md (교차 발원 trex-core 중장기 cold-start) + - app/api/v1_user_info.py / app/domain/models.py(BrokerCredential) / app/api/v1_portfolios.py(daemon-scope 선례) + +# 10. 사용자 결정 필요 항목 요약 + +## DECIDE +- ~~PK 전환 형태 (OQ-1)~~ → **RESOLVED 2026-06-28: PK `(daemon_id, broker)` + daemon-scoped 강제(폴백 없음), 단계적 마이그레이션** +- ~~엔드포인트 형태 (OQ-2)~~ → **RESOLVED 2026-06-28: 기존 `/v1/user_info` daemon-scoped 확장** +- ~~부트스트랩 통합 (OQ-3)~~ → **Deferred: trex-core 소비자 Discovery 공동 결정** + +## CONFIRM +- ~~1데몬1계좌 불변식~~ → **RESOLVED 2026-06-28: 확정(A-2 confirmed) → 단일 broker 응답** +- ~~trex-core 소비자측 분리~~ → **RESOLVED 2026-06-28: 별도 trex-core Discovery로 분리 확정** + +## DATA +- ~~기존 /v1/user_info 소비자 인벤토리~~ → **RESOLVED 2026-06-28: 유일 소비자=trex-core hub_read.whoami(daemon_id만 읽음), brokers 미참조 → reshape 안전** + +# 11. Discovery Freeze +REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록한다. + +## 섹션 참조 +- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록) +- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록) +- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준) +- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크) +- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약) + +## Freeze 플래그 +- Handoff Decision: REQ Drafting 진행 가능 +- Handoff Rationale: confirm-discovery 완료(2026-06-28). DECIDE 전건 해소 — OQ-1(PK `(daemon_id, broker)`+폴백없음), OQ-2(기존 `/v1/user_info` 확장), OQ-3(Deferred). CONFIRM 전건 — A-2(1데몬1계좌→단일 broker), trex-core 소비자측 별도 Discovery 분리. DATA(소비자 인벤토리) 해소. 잔여 Open 0건 → REQ 초안 인계 가능. +- Ready for REQ Drafting: true +- Confirmed By: 팀장(Claude) +- Confirmed At (KST): 2026-06-28 diff --git a/docs/discovery/index.md b/docs/discovery/index.md index 9c7e3f0..574ffac 100644 --- a/docs/discovery/index.md +++ b/docs/discovery/index.md @@ -15,3 +15,4 @@ | dcy-007_20260616_daily-trade-history-and-realized-pnl-query-api | 20260616 | 일일 거래내역 + 실현 PnL 조회 API (운영 리포트/프론트 데이터 소스, issue #11) | [Discovery](./dcy-007_20260616_daily-trade-history-and-realized-pnl-query-api.md) | confirmed | | dcy-008_20260625_current-holding-wac-cost-basis-read | 20260625 | 현재 보유 종목별 평단(WAC) 조회 API — trex-core 중장기 cold-start 슬롯 평단 seed 의존(교차 발원 trex-core dcy-034 FR-7). GET /v1/portfolios/{id}/holdings/cost-basis | [Discovery](./dcy-008_20260625_current-holding-wac-cost-basis-read.md) | confirmed | | dcy-009_20260627_user-read-asset-catalog-and-daemons | 20260627 | 사용자용 read 2종 — GET /v1/assets require_read 완화 + 사용자 가용 데몬 목록 신규(교차 발원 trex-web 포트폴리오 관리 dcy-007) | [Discovery](./dcy-009_20260627_user-read-asset-catalog-and-daemons.md) | confirmed | +| dcy-010_20260627_daemon-scoped-broker-credential-provisioning | 20260627 | daemon-scoped 브로커(KIS) 인증정보 소유·전달 — 다중 인스턴스 trex-core가 daemon별 다른 계좌로 거래 (교차 발원 trex-core 중장기 멀티 인스턴스) | [Discovery](./dcy-010_20260627_daemon-scoped-broker-credential-provisioning.md) | confirmed |