13 KiB
13 KiB
id, status
| id | status |
|---|---|
| dcy-0004 | confirmed |
포트폴리오 생성 시 연결 데몬 persistence 및 배정 검증
Problem
포트폴리오 생성 요청에 선택한 managed_by_daemon_id가 포함되어도 portfolio-hub의 POST /v1/portfolios handler와 repository create port 사이에서 해당 값이 누락되어 persistence되지 않는다. 비-null PATCH 재지정은 전달되므로 사용자는 생성 직후 다시 편집해야 연결 데몬을 반영할 수 있다.
이 Discovery는 요청자가 승인한 GitHub 이슈 #27을 canonical ProofLine 문서로 포착한다. 아래 Scope의 승인된 이슈 본문 전체는 구조적 heading만 canonical H2 충돌을 피하도록 한 단계 낮췄으며, 원문의 비어 있지 않은 모든 내용과 승인 경계를 순서대로 보존한다.
Evidence
승인 원문과 provenance
- 원문: https://github.com/fourblocks2020/portfolio-hub/issues/27
- 원문 제목:
trex-web : 포트폴리오 추가시 연결 데몬을 설정하였음에도 지정되지 않음 - 원문 상태:
open - 원문 최초 작성:
2026-07-21T14:42:15Z - 승인 본문 최종 갱신:
2026-08-15T03:03:53Z - 원문 body SHA-256:
17668fbfac2419713e8247637fbf05631d93a71bdb74db0fe122743fba8d44e4 - 원문 승인 상태:
APPROVED, 승인자genichin, 승인 시각2026-08-15T03:02:35Z, unresolved Open Questions 없음. - 이슈는 최초
fourblocks2020/trex-web#10에서 생성됐으나 근본 원인이portfolio-hub의 create persistence 경계로 확인되어 현재fourblocks2020/portfolio-hub#27로 이전됐다. - exact 승인 본문 snapshot은
evidence/issue-27-approved-discovery.md에 보존한다. - source review 기준 commit은
portfolio-hub2a1a637fb9fb19944991cb902530faf72f0e9f84이며, 이 문서 작성 시 product 구현은 수행하지 않았다.
승인·handoff 경계
- 요청자는 OQ-001~OQ-004의 단일 선택을 완료하고 Discovery 검토·승인을 명시했다.
trex-web관련 코드·타입·화면·테스트 수정은 명시적으로 Out of Scope다.- 이 confirmed Discovery는 다음 REQ 작성 및 검토를 위한 scope authority다. 구현 완료, approved REQ, active/satisfied AC, implementation/delivery closure, main 반영, push, PR, migration 실행, 배포 또는 release 완료를 주장하지 않는다.
Scope
승인된 이슈 본문 전체
원래 제보
편집으로 다시 지정해야 반영 됨.
ProofLine Discovery
- Status: APPROVED
- Source: 프로젝트 보드
trex/ 현재fourblocks2020/portfolio-hub#27(최초fourblocks2020/trex-web#10에서 이전) - 검토 기준:
portfolio-hub2a1a637fb9fb19944991cb902530faf72f0e9f84, 증상 경계 확인용trex-webb72ccd7cd639447e71d7e785857d2c503e3a1841 - 승인 경계: 이 승인은 아래
portfolio-hubDiscovery 범위를 승인한다. 구현 완료,trex-web변경, main 반영, push, PR, 배포 또는 release 승인을 의미하지 않는다.
Problem
포트폴리오 추가 화면에서 사용자가 연결 데몬을 선택해 생성해도 생성된 포트폴리오의 managed_by_daemon_id가 저장되지 않는다. 같은 포트폴리오를 편집하여 비-null 데몬을 다시 지정하면 반영된다.
프런트엔드는 생성 요청에 선택값을 포함하지만, portfolio-hub의 POST /v1/portfolios 처리기가 해당 값을 저장소 생성 호출로 전달하지 않는다. 반면 PATCH /v1/portfolios/{portfolio_id}는 비-null managed_by_daemon_id를 update fields에 포함하므로 “생성에서는 누락되고 편집에서는 반영”되는 제보와 코드 경로가 일치한다.
Evidence
- Live issue: https://github.com/fourblocks2020/portfolio-hub/issues/27
- 원래 제보: “편집으로 다시 지정해야 반영 됨.”
- 최초 작성:
2026-07-21T14:42:15Z
trex-web/features/portfolio/portfolio-create-form.tsx:83-91- 선택한 데몬을
managed_by_daemon_id: selectedDaemonId || null로 생성 mutation에 전달한다.
- 선택한 데몬을
trex-web/lib/api/portfolio-hub.ts:87-95,251-256- 생성 요청 타입과
POST /v1/portfolioswrapper가managed_by_daemon_id를 허용하고 payload를 그대로 전송한다.
- 생성 요청 타입과
portfolio-hub/app/schemas/v1.py:235-258- 생성·수정 schema 모두
managed_by_daemon_id입력을 허용한다.
- 생성·수정 schema 모두
portfolio-hub/app/api/v1_portfolios.py:170-183- 생성 handler가
repo.create_portfolio(...)에managed_by_daemon_id를 전달하지 않는다.
- 생성 handler가
portfolio-hub/app/repositories/portfolio_repository.py:150-165- 현재 생성 port 시그니처에도
managed_by_daemon_id가 없다.
- 현재 생성 port 시그니처에도
portfolio-hub/app/api/v1_portfolios.py:222-235- 편집 handler는
exclude_none=True로 얻은 비-null fields를repo.update_portfolio(...)에 전달하므로 비-null 데몬 재지정은 저장된다.
- 편집 handler는
portfolio-hub/app/api/v1_daemons.py:26-56- 일반 daemon 목록은 호출자 소유 범위이며 active와 inactive를
is_active로 구분해 반환한다.
- 일반 daemon 목록은 호출자 소유 범위이며 active와 inactive를
portfolio-hub/docs/data-model.md:231-259- daemon ID는 server-generated UUID이고, daemon은 사용자 소유이며, service token 바인딩에는 존재·active·동일 소유자 규칙이 적용된다. 비활성화는 토큰을 revoke하지만 기존 portfolio reference 자동 제거는 규정하지 않는다.
portfolio-hub/docs/interface-contract.md:301-315- 포트폴리오 생성·수정 API가
managed_by_daemon_id를 입력으로 공개하며 portfolio 소유권과 오류 계약의 기존 경계를 정의한다.
- 포트폴리오 생성·수정 API가
승인된 결정
- 이 이슈는
portfolio-hub가 소유하는 repo-local 수정만 다룬다.trex-web코드·테스트와 필수strategy_idUI 적응은 이 이슈의 범위 밖이다. - 포트폴리오 생성·수정에서 비-null daemon을 지정하려면 지정 시점에 daemon이 존재하고 active이며 포트폴리오 소유자와 같은 사용자가 소유해야 한다.
- admin이 다른 사용자의 포트폴리오를 생성·수정할 때도 daemon 소유자는 호출 admin이 아니라 대상 포트폴리오 소유자와 일치해야 한다.
- 미존재 또는 배정 권한이 없는 foreign daemon은 존재를 숨겨 HTTP 404, 소유 범위 안의 inactive daemon은 HTTP 409, UUID 형식이 아닌 daemon ID는 HTTP 422로 반환한다.
PATCH의 명시적managed_by_daemon_id: null해제 결함은 별도 이슈로 추적하고 이번 범위에 포함하지 않는다.
Scope
POST /v1/portfolios가 요청의 비-nullmanaged_by_daemon_id를 생성 persistence까지 전달한다.- 생성과 PATCH의 비-null daemon 지정·재지정은 같은 validation 계약을 사용한다.
- 요청 daemon ID는 daemon의 server-generated UUID 형식이어야 한다.
- daemon은 portfolio persistence 시점에 존재하고 active여야 하며
daemon.owner_user_id == portfolio.owner_user_id여야 한다. - admin의
user_idoverride 생성에서는 최종 결정된 targetowner_user_id를 daemon 소유권 비교 기준으로 사용한다. - 미존재·foreign daemon은 HTTP 404, inactive daemon은 HTTP 409, malformed daemon ID는 HTTP 422로 실패한다.
- validation 실패 시 portfolio 생성·수정과 관련 부수 상태가 부분 반영되지 않는다.
- daemon validation과 portfolio write는 동시 변경에 대해 하나의 원자적 판정 경계를 가져야 한다. assignment가 먼저 확정된 뒤 daemon이 비활성화되면 기존 deactivation 계약에 따라 service token은 revoke되고 portfolio reference는 보존된다. 비활성화가 먼저 확정되면 새 assignment는 HTTP 409로 실패한다.
- 데몬을 선택하지 않은 생성 요청은
managed_by_daemon_id = null인 unassigned portfolio를 생성하는 기존 동작을 유지한다. - 성공한 생성 응답과 후속 portfolio 조회는 같은
managed_by_daemon_id를 반환한다. - 기존 비-null PATCH 지정·재지정 경로에는 위 validation을 적용하되, 포트폴리오 소유권·disabled 보호·service/client 거부 등 기존 mutation 계약을 유지한다.
portfolio-hub단위/API 테스트와 disposable PostgreSQL 통합 테스트로 성공·미선택·malformed·미존재·foreign·inactive·admin target-owner·동시성·POST → GETpersistence를 검증한다.
승인된 수용 기준 후보
- 소유자의 active daemon을 선택해 포트폴리오를 생성하면 HTTP 201 응답의
managed_by_daemon_id가 선택한 ID와 같다. - 같은 포트폴리오를 후속 조회해도 선택한 daemon ID가 보존된다.
- daemon 미선택 생성은
managed_by_daemon_id = null을 유지한다. - UUID 형식이 아닌 daemon ID는 HTTP 422이며 portfolio가 생성·수정되지 않는다.
- 미존재 또는 portfolio 소유자와 다른 사용자의 daemon ID는 HTTP 404이며 portfolio가 생성·수정되지 않는다.
- portfolio 소유자 범위의 inactive daemon ID는 HTTP 409이며 portfolio가 생성·수정되지 않는다.
- admin target-user 생성·수정은 target portfolio owner 소유의 active daemon만 허용한다.
- 비-null PATCH 지정·재지정도 생성과 동일한 daemon validation과 오류 계약을 적용한다.
- assignment와 daemon 비활성화가 경합해도 inactive daemon에 새로운 assignment가 확정되는 중간 상태를 허용하지 않는다.
- assignment 확정 후 daemon 비활성화는 기존 portfolio reference를 자동 제거하지 않고 기존 token-revocation 계약을 유지한다.
- 실제 PostgreSQL persistence 경계에서
POST → GET보존과 실패 시 무변경이 검증된다. - 기존 portfolio 생성·수정의 인증, 소유권, strategy, asset, disabled-state 계약이 회귀하지 않는다.
Out of Scope
trex-web코드, 타입, 화면 또는 테스트 변경trex-web의/v1/strategies조회·선택과 필수strategy_id계약 적응PATCH /v1/portfolios/{id}의 명시적managed_by_daemon_id: null해제 지원- 데몬 실행·중지·재시작 또는 배포 자동화
- 데몬 credential 관리 정책 변경
- 포트폴리오의 자동 데몬 배정
- daemon 비활성화 시 기존 portfolio reference 자동 해제 또는 다른 daemon으로 자동 이전
- 기존 데이터 전체에 대한 일괄 backfill
- 내부
daemons또는portfolios식별자·테이블 구조 변경 - 이 Discovery 승인만으로 구현, main 반영, push, PR, migration 실행, 배포 또는 release 수행
Risks and Dependencies
managed_by_daemon_id는 DB-level FK가 없는 soft reference이므로 validation과 write를 분리하면 daemon 상태·소유권 변경과의 TOCTOU 위험이 생긴다. persistence transaction과 적절한 row-lock/동등한 직렬화 경계가 필요하다.- daemon 소유자 재할당 기능이 있으므로 portfolio assignment와 동시 실행될 때도 최종 관계가 동일 소유자 규칙을 위반하지 않아야 한다.
trex-web의 필수strategy_id계약 드리프트는 별도 범위다. 이번 backend 동작은 유효한strategy_id를 포함한 직접 API 요청으로 독립 검증한다.- 명시적 null 해제는 알려진 별도 결함으로 남으며 이번 승인 결과를 그 기능의 완료로 해석하지 않는다.
Approval State
- Status: APPROVED
- Approved by: genichin
- Approved at: 2026-08-15T03:02:35Z
- Approval basis: OQ-001=A, OQ-002=A, OQ-003=A, OQ-004=A의 단일 선택과
trex-web관련 수정·테스트를 Out of Scope로 두라는 요청자의 명시적 지시 - Unresolved Open Questions: 없음
- Confirmation: Discovery 검토·승인 완료
- Authority boundary: 위
portfolio-hubDiscovery 범위만 승인하며 구현 완료, lifecycle closure, main 반영, push, PR, 배포 또는 release를 의미하지 않는다.
Out of Scope
Canonical 경계
- 위 승인 원문 전체의
Out of Scope와Authority boundary를 그대로 적용한다. - 특히
trex-web코드·타입·화면·테스트,/v1/strategiesUI 적응, PATCH의 명시적managed_by_daemon_id: null해제, 자동 배정·이전·해제, backfill, 배포·release는 이 Line의 Discovery 범위가 아니다. - 이 단계에서는 REQ·AC를 생성하거나 승인하지 않으며 product source, test, runtime configuration 또는 schema를 변경하지 않는다.
Risks and Unknowns
Risks
managed_by_daemon_id가 DB-level FK 없는 soft reference이므로 validation과 write가 분리되면 daemon 비활성화·소유자 변경과 TOCTOU가 발생할 수 있다.- daemon 소유자 재할당과 portfolio assignment가 경합해도 최종 관계가 동일 소유자 규칙을 위반하지 않는 원자적 판정 경계가 필요하다.
trex-web의 필수strategy_id계약 드리프트는 별도 범위이므로 이번 backend 검증은 유효한strategy_id를 포함한 직접 API 요청으로 독립 수행해야 한다.- 명시적 null 해제 결함은 알려진 별도 문제로 남으며 이번 Line 완료로 해석하지 않는다.
Assumptions
- daemon ID의 canonical 형식은 기존 data model의 server-generated UUID다.
- daemon 비활성화가 assignment 확정 뒤 발생하면 기존 reference는 보존하고 service token revoke 계약이 적용된다.
- 위 가정은 승인된 이슈 본문과 현재 interface/data-model contract에서 직접 유도되며 추가 Open Question은 없다.