docs(dcy-010): draft-req + confirm-req — REQ-046~051 Approved (daemon-scoped broker credential provisioning)
Adds SRS specification documents for daemon-scoped broker credential management: - REQ-046: Broker credential daemon_id field - REQ-047: Migration PKv1→PKv2 with daemon_id - REQ-048: Backfill existing broker credentials with daemon_id - REQ-049: User info endpoint including daemon-scoped credentials - REQ-050: Credential CLI --daemon-id option - REQ-051: Security isolation per daemon boundary Updates dcy-010 discovery document with generated REQ references and register. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VHtM5Ws1HKCKQ4JGi6GRb5
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
# REQ-049: GET /v1/user_info daemon-scoped 단일 broker 전달로 확장
|
||||
|
||||
- Status: Approved
|
||||
- Type: Interface
|
||||
- Priority: High
|
||||
- Owner: Justin Ko
|
||||
|
||||
## Intent
|
||||
- 기존 `GET /v1/user_info`가 `user_id` 기반으로 `brokers: list[BrokerItem]`을 반환한다. 이를 service token의 `auth.daemon_id`로 매칭된 단일 broker 인증정보를 반환하는 daemon-scoped 전달로 확장한다.
|
||||
- "1 데몬 = 1 계좌" 불변식(dcy-010 A-2 confirmed)에 따라 응답을 `broker: BrokerItem | None`(단일)으로 정규화하여, 같은 user의 두 데몬이 서로 다른 KIS 계좌를 각각 수신할 수 있게 한다.
|
||||
- 유일 소비자인 trex-core `hub_read.whoami()`는 `daemon_id`만 읽으며 `brokers` 필드를 참조하지 않으므로, 응답 형태 변경이 소비자에게 무영향임이 확인된 안전한 변경이다(dcy-010 OQ-2 Answered).
|
||||
|
||||
## Requirement
|
||||
- `GET /v1/user_info`의 응답 스키마를 다음과 같이 변경한다:
|
||||
- `brokers: list[BrokerItem]` 필드를 제거하고 `broker: BrokerItem | None` 단일 필드로 대체한다.
|
||||
- `broker`는 service token의 `auth.daemon_id`에 매칭된 인증정보 1건을 복호화하여 반환한다. `daemon_id`에 매칭된 인증정보가 없으면 `broker: null`을 반환한다(graceful degrade — NFR-3).
|
||||
- service token에 `daemon_id`가 없는 경우(None), 엔드포인트는 HTTP 403 Forbidden을 반환한다. 기존 "daemon_id 없어도 200 반환" 동작을 `broker` 필드에 대해서는 적용하지 않으며, `daemon_id` 자체가 없으면 403으로 거부한다.
|
||||
- `app/schemas/v1.py`의 `UserInfoResponse` 모델을 `broker: BrokerItem | None = None` 필드로 변경하고 `brokers: list[BrokerItem]`를 제거한다.
|
||||
- `app/api/v1_user_info.py`의 엔드포인트 구현이 `auth.daemon_id`로 `list_broker_credentials(daemon_id=auth.daemon_id)` 호출 후 결과에서 단일 항목을 복호화·반환하도록 변경한다.
|
||||
- 인증정보 복호화 실패(잘못된 키, 페이로드 손상)는 기존 graceful degrade 정책을 유지한다: 경고 로깅 후 `credential_payload: null`을 포함한 `BrokerItem`을 반환한다(응답을 크래시시키지 않음).
|
||||
|
||||
## Acceptance Criteria
|
||||
1. Given daemon_id가 바인딩된 service token으로 GET /v1/user_info 호출, 해당 daemon_id에 매칭된 broker 인증정보가 존재하면, When 호출, Then HTTP 200 + `broker` 필드에 단일 `BrokerItem`(account_ref, credential_payload, credential_version, last_rotated_at 포함) 반환.
|
||||
2. Given daemon_id가 바인딩된 service token으로 호출, 해당 daemon_id에 매칭된 인증정보가 없으면, When 호출, Then HTTP 200 + `broker: null` 반환 (graceful degrade, 크래시 없음).
|
||||
3. Given daemon_id가 없는(None) service token으로 GET /v1/user_info 호출, When 호출, Then HTTP 403 Forbidden 반환.
|
||||
4. Given 같은 user 소유의 daemon A, daemon B에 각각 다른 KIS account_ref가 등록된 상황, When daemon A의 service token으로 호출, Then `broker.account_ref`가 daemon A에 등록된 account_ref와 일치함.
|
||||
5. Given 같은 상황에서 daemon B의 service token으로 호출, When 호출, Then `broker.account_ref`가 daemon B에 등록된 account_ref와 일치함 (daemon A의 계좌 반환하지 않음).
|
||||
6. Given `UserInfoResponse` 응답 스키마, When 응답 JSON 확인, Then `brokers` 키가 존재하지 않고 `broker` 단일 키가 존재한다.
|
||||
7. Given 인증정보 복호화 실패(잘못된 key), When daemon_id에 매칭된 인증정보가 있으나 복호화 실패, Then HTTP 200 + `broker.credential_payload: null` 반환 (경고 로그 기록, 크래시 없음).
|
||||
8. Given service 토큰이 아닌 user 토큰(web 토큰)으로 호출, When 호출, Then HTTP 403 Forbidden 반환 (기존 service-token gate 유지).
|
||||
9. Given `runtime_id`, `daemon_id`, `trading_enabled`, `user_id`, `username`, `permission` 필드, When GET /v1/user_info 호출, Then 이 필드들은 기존과 동일하게 반환된다 (broker 변경이 나머지 필드에 영향 없음, 회귀 없음).
|
||||
|
||||
## Impacted Area
|
||||
- Docs:
|
||||
- `docs/interface-contract.md` — `GET /v1/user_info` 섹션: `brokers: list` → `broker: object|null` 응답 스키마 갱신, daemon_id 없음 → 403 정책 명시.
|
||||
- `docs/data-model.md` — `BrokerCredential` State Rules 갱신: 전달 경로에 daemon-scoped 언급 추가.
|
||||
- Modules:
|
||||
- `app/schemas/v1.py` — `UserInfoResponse` 모델: `brokers: list[BrokerItem]` → `broker: BrokerItem | None = None`.
|
||||
- `app/api/v1_user_info.py` — 엔드포인트 구현: user_id 기반 list → daemon_id 기반 단일 조회, daemon_id None → 403.
|
||||
- Tests:
|
||||
- `GET /v1/user_info` 단위/통합 테스트 갱신: daemon_id 있음/없음, broker 있음/없음, 2데몬 격리(AC-4/5), 복호화 실패(AC-7), 회귀(AC-9).
|
||||
- 기존 `brokers` 필드 참조 테스트 → `broker` 단일 필드로 변경.
|
||||
|
||||
## Notes
|
||||
- **파괴적 변경(breaking change) 해당 여부**: `brokers: list` → `broker: object|null`는 API 응답 스키마 변경이다. 그러나 dcy-010 OQ-2에서 유일 소비자 trex-core `hub_read.whoami()`가 `daemon_id`만 읽고 `brokers` 필드를 참조하지 않음이 확인되었으므로, **실질적 소비자 영향은 없다**. 다만 이 REQ 구현 전 trex-core 소비자 코드 재확인을 권장한다.
|
||||
- daemon_id 없음 → 403 정책은 dcy-010 FR-2 및 universe 인가(REQ-028 `managed_by_daemon_id` 일치 요구)와 동일한 규율이다.
|
||||
- `broker: null` graceful degrade(AC-2)는 dcy-010 NFR-3(인증정보 부재가 `daemon_id`, `trading_enabled` 필수 필드를 크래시시키지 않음)을 구현한다. `broker: null`은 그날 거래 개시 차단 신호로 소비자(trex-core)가 해석한다. 이 해석 로직은 trex-core Discovery(별도)에서 구현한다.
|
||||
- OQ-3(부트스트랩 통합 — universe+인증정보 단일 응답): dcy-010 Deferred. 이 REQ는 인증정보 전달 형태만 확정하며, 부트스트랩 통합은 다루지 않는다.
|
||||
- Source: dcy-010 FR-2, OQ-2 Answered, A-2, NFR-3.
|
||||
|
||||
## Change Log
|
||||
|
||||
### CHG-20260628-01
|
||||
- Date: 2026-06-28
|
||||
- Author: Justin Ko
|
||||
|
||||
#### Change Summary
|
||||
- 신규 생성 (Proposed) — dcy-010 FR-2 산출물.
|
||||
|
||||
#### Intent
|
||||
- dcy-010 FR-2(기존 GET /v1/user_info daemon-scoped 확장, 단일 broker 정규화)를 Interface REQ로 정규화한다.
|
||||
|
||||
#### Acceptance Criteria Delta
|
||||
- Added:
|
||||
- AC-1 ~ AC-9 전체 (신규 초안)
|
||||
- Updated: 없음
|
||||
- Removed: 없음
|
||||
|
||||
#### Impacted Area Delta
|
||||
- Docs:
|
||||
- `docs/srs/Interface/req-049_user-info-daemon-scoped-broker-delivery.md` 신규 생성
|
||||
- `docs/interface-contract.md`, `docs/data-model.md` 갱신 예고
|
||||
- Modules:
|
||||
- `app/schemas/v1.py` (`UserInfoResponse` 변경), `app/api/v1_user_info.py` (로직 변경)
|
||||
- Tests:
|
||||
- user_info 엔드포인트 테스트 전면 갱신 신규
|
||||
|
||||
#### Delivery Trace Delta
|
||||
- Batches: 미정 (suggest-batch-reqs 대기)
|
||||
- Releases: 미정
|
||||
|
||||
#### Revalidation Impact
|
||||
- Reverification Needed: no
|
||||
- Existing Implementation Invalidated: no (기존 `brokers` 필드 구현은 이 REQ 구현 시 대체됨)
|
||||
- Status Recommendation: keep-current
|
||||
- Follow-up Action: confirm-req 후 batch 배정. REQ-046(모델), REQ-047(스키마) 선행 의존. trex-core 소비자 코드 재확인.
|
||||
|
||||
#### Validation Plan
|
||||
- pytest로 AC-1~9 각 케이스 확인. 2데몬 격리(AC-4/5) 특히 중요.
|
||||
|
||||
### CHG-20260628-02
|
||||
- Date: 2026-06-28
|
||||
- Author: 팀장(Claude)
|
||||
- confirm-req: `Proposed` → `Approved`. dcy-010 FR-2/OQ-2/A-2/NFR-3 추적. brokers→단일 broker reshape, daemon_id 없음 403, 2데몬 격리(AC-4/5), graceful degrade(AC-2) 적절. 구현 전 trex-core 소비자 재확인 권고(Notes) 유효. blocker 없음.
|
||||
@@ -0,0 +1,90 @@
|
||||
# REQ-050: credential CLI --daemon-id 옵션 추가
|
||||
|
||||
- Status: Approved
|
||||
- Type: Interface
|
||||
- Priority: High
|
||||
- Owner: Justin Ko
|
||||
|
||||
## Intent
|
||||
- 현재 `pfhub credential set / list / remove` CLI는 `--user` 옵션만으로 인증정보를 식별한다. daemon-scoped 소유 전환 이후 운영자가 daemon_id를 지정하여 특정 데몬의 인증정보를 등록/조회/삭제할 수 있도록 `--daemon-id` 옵션을 추가한다.
|
||||
- REQ-047 PK 전환 이후 `broker_credentials` PK가 `(daemon_id, broker)`로 변경되므로, 인증정보 등록/삭제 시 `daemon_id`가 필수 식별자가 된다.
|
||||
|
||||
## Requirement
|
||||
- `pfhub credential set` 커맨드에 `--daemon-id` 옵션(required, `--daemon-id <daemon_slug_or_id>`)을 추가한다. 이 옵션의 값을 `daemon_id`로 사용하여 `upsert_broker_credential(daemon_id=..., ...)` 호출 시 전달한다.
|
||||
- `pfhub credential list` 커맨드에 `--daemon-id` 옵션(optional)을 추가한다. 값이 제공되면 해당 daemon_id의 인증정보만 반환한다. 미제공 시 `--user` 소유 전체 인증정보를 반환한다(기존 동작 유지).
|
||||
- `pfhub credential remove` 커맨드에 `--daemon-id` 옵션(required)을 추가한다. 기존 `(user_id, broker)` 기반 삭제에서 `(daemon_id, broker)` 기반 삭제로 변경한다.
|
||||
- `--daemon-id` 옵션 값은 daemon_slug 또는 daemon UUID 중 하나를 허용한다. 내부에서 `Daemon` 조회로 daemon_id(UUID)를 해석한다. daemon이 존재하지 않으면 오류 메시지 출력 후 exit 1.
|
||||
- CLI 출력에 `daemon_id` 필드를 포함한다(기존 `user_id` 출력 유지, 추가).
|
||||
|
||||
## Acceptance Criteria
|
||||
1. Given `pfhub credential set --user u1 --daemon-id d1 --broker kis --account-ref "123" --app-key "k" --app-secret "s"` 실행, When 명령 성공, Then `broker_credentials`에 `daemon_id=d1.id, broker="kis"` 행이 upsert된다.
|
||||
2. Given `pfhub credential list --user u1 --daemon-id d1` 실행, When 명령 성공, Then daemon_id=d1인 인증정보만 출력된다(daemon_id=d2 인증정보 미출력).
|
||||
3. Given `pfhub credential list --user u1` (daemon-id 미지정) 실행, When 명령 성공, Then 해당 user의 모든 인증정보가 출력된다(기존 동작 유지).
|
||||
4. Given `pfhub credential remove --user u1 --daemon-id d1 --broker kis` 실행, When 명령 성공, Then `broker_credentials`에서 `daemon_id=d1.id, broker="kis"` 행이 삭제된다.
|
||||
5. Given 존재하지 않는 daemon_slug를 `--daemon-id`에 지정, When 명령 실행, Then 오류 메시지("데몬을 찾을 수 없습니다") 출력 후 exit 1.
|
||||
6. Given `pfhub credential set` 실행 시 `--daemon-id` 옵션 미제공, When 명령 실행, Then 오류 메시지(`--daemon-id` required) 출력 후 exit 1. (REQ-047 PK 전환 이후 daemon_id 없이 등록 불가)
|
||||
7. Given `pfhub credential set` 실행 성공, When CLI 출력 확인, Then `daemon_id`, `broker`, `account_ref`, `credential_version`, `last_rotated_at` 필드가 모두 포함된다.
|
||||
|
||||
## Impacted Area
|
||||
- Docs:
|
||||
- `docs/interface-contract.md` — CLI credential surface 섹션: `--daemon-id` 옵션 추가, daemon-scoped 등록/조회/삭제 예시.
|
||||
- Modules:
|
||||
- `app/cli/commands/credential.py` — `set_credential`, `list_credentials`, `remove_credential` 함수에 `--daemon-id` 파라미터 추가. daemon 조회 로직 추가.
|
||||
- `app/repositories/postgres.py` — `get_daemon_by_slug(daemon_slug)` 또는 동등한 메서드가 필요(기존 존재 여부 확인 후 추가 또는 재사용).
|
||||
- Tests:
|
||||
- `set_credential --daemon-id` 단위 테스트(mock repo).
|
||||
- `list_credentials --daemon-id` 필터 단위 테스트.
|
||||
- `remove_credential --daemon-id` 단위 테스트.
|
||||
- daemon 미존재 오류 케이스(AC-5).
|
||||
- `--daemon-id` 미제공 시 오류(AC-6).
|
||||
|
||||
## Notes
|
||||
- `--daemon-id` 옵션은 daemon_slug(사람이 읽기 쉬운 식별자) 또는 daemon UUID를 허용할지 여부는 batch design 단계에서 기존 `daemons` 테이블 스키마를 확인하여 확정한다. 기존 CLI의 `--user` 옵션이 username을 받아 내부에서 user_id로 변환하는 패턴과 동일하게 설계한다.
|
||||
- REQ-047 PK 전환 이전 단계(daemon_id 컬럼은 있으나 nullable)에서는 `--daemon-id` 미제공 시에도 set/remove를 허용할지 결정이 필요하다. 단계적 마이그레이션 기간 중 호환성 정책은 batch design에서 확정한다. 이 REQ의 AC-6은 PK 전환(REQ-047 Step 2) 완료 이후 상태를 기준으로 한다.
|
||||
- `pfhub credential list` daemon_id 미지정 동작(전체 조회, AC-3)은 PK 전환 이후 daemon_id 컬럼이 NOT NULL이 되어도 유지된다(admin 관리 목적).
|
||||
- Source: dcy-010 FR-3, §4 C3.
|
||||
|
||||
## Change Log
|
||||
|
||||
### CHG-20260628-01
|
||||
- Date: 2026-06-28
|
||||
- Author: Justin Ko
|
||||
|
||||
#### Change Summary
|
||||
- 신규 생성 (Proposed) — dcy-010 FR-3 산출물.
|
||||
|
||||
#### Intent
|
||||
- dcy-010 FR-3(인증정보 관리 경로에 daemon_id 바인딩 추가)을 CLI Interface REQ로 정규화한다.
|
||||
|
||||
#### Acceptance Criteria Delta
|
||||
- Added:
|
||||
- AC-1 ~ AC-7 전체 (신규 초안)
|
||||
- Updated: 없음
|
||||
- Removed: 없음
|
||||
|
||||
#### Impacted Area Delta
|
||||
- Docs:
|
||||
- `docs/srs/Interface/req-050_credential-cli-daemon-id-option.md` 신규 생성
|
||||
- `docs/interface-contract.md` 갱신 예고
|
||||
- Modules:
|
||||
- `app/cli/commands/credential.py` (옵션 추가), `app/repositories/postgres.py` (daemon 조회 메서드)
|
||||
- Tests:
|
||||
- credential CLI daemon-id 옵션 단위 테스트 신규
|
||||
|
||||
#### Delivery Trace Delta
|
||||
- Batches: 미정 (suggest-batch-reqs 대기)
|
||||
- Releases: 미정
|
||||
|
||||
#### Revalidation Impact
|
||||
- Reverification Needed: no
|
||||
- Existing Implementation Invalidated: no
|
||||
- Status Recommendation: keep-current
|
||||
- Follow-up Action: confirm-req 후 batch 배정. REQ-046(모델), REQ-047(스키마) 선행 의존. --daemon-id 필수/선택 단계 정책은 design에서 확정.
|
||||
|
||||
#### Validation Plan
|
||||
- pytest mock repo 기반 단위 테스트 AC-1~7. daemon 미존재 오류 케이스(AC-5) 포함.
|
||||
|
||||
### CHG-20260628-02
|
||||
- Date: 2026-06-28
|
||||
- Author: 팀장(Claude)
|
||||
- confirm-req: `Proposed` → `Approved`. dcy-010 FR-3 추적. set/list/remove --daemon-id 적절. slug vs UUID 해석 + REQ-047 과도기(daemon_id nullable 구간) --daemon-id 필수/선택 정책은 batch design 위임 — AC 무효화 아님(비차단).
|
||||
Reference in New Issue
Block a user