Files

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-hub 2a1a637fb9fb19944991cb902530faf72f0e9f84이며, 이 문서 작성 시 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-hub 2a1a637fb9fb19944991cb902530faf72f0e9f84, 증상 경계 확인용 trex-web b72ccd7cd639447e71d7e785857d2c503e3a1841
  • 승인 경계: 이 승인은 아래 portfolio-hub Discovery 범위를 승인한다. 구현 완료, 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/portfolios wrapper가 managed_by_daemon_id를 허용하고 payload를 그대로 전송한다.
  • portfolio-hub/app/schemas/v1.py:235-258
    • 생성·수정 schema 모두 managed_by_daemon_id 입력을 허용한다.
  • portfolio-hub/app/api/v1_portfolios.py:170-183
    • 생성 handler가 repo.create_portfolio(...)에 managed_by_daemon_id를 전달하지 않는다.
  • portfolio-hub/app/repositories/portfolio_repository.py:150-165
    • 현재 생성 port 시그니처에도 managed_by_daemon_id가 없다.
  • portfolio-hub/app/api/v1_portfolios.py:222-235
    • 편집 handler는 exclude_none=True로 얻은 비-null fields를 repo.update_portfolio(...)에 전달하므로 비-null 데몬 재지정은 저장된다.
  • portfolio-hub/app/api/v1_daemons.py:26-56
    • 일반 daemon 목록은 호출자 소유 범위이며 active와 inactive를 is_active로 구분해 반환한다.
  • 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 소유권과 오류 계약의 기존 경계를 정의한다.

승인된 결정

  1. 이 이슈는 portfolio-hub가 소유하는 repo-local 수정만 다룬다. trex-web 코드·테스트와 필수 strategy_id UI 적응은 이 이슈의 범위 밖이다.
  2. 포트폴리오 생성·수정에서 비-null daemon을 지정하려면 지정 시점에 daemon이 존재하고 active이며 포트폴리오 소유자와 같은 사용자가 소유해야 한다.
  3. admin이 다른 사용자의 포트폴리오를 생성·수정할 때도 daemon 소유자는 호출 admin이 아니라 대상 포트폴리오 소유자와 일치해야 한다.
  4. 미존재 또는 배정 권한이 없는 foreign daemon은 존재를 숨겨 HTTP 404, 소유 범위 안의 inactive daemon은 HTTP 409, UUID 형식이 아닌 daemon ID는 HTTP 422로 반환한다.
  5. PATCH의 명시적 managed_by_daemon_id: null 해제 결함은 별도 이슈로 추적하고 이번 범위에 포함하지 않는다.

Scope

  • POST /v1/portfolios가 요청의 비-null managed_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_id override 생성에서는 최종 결정된 target owner_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 → GET persistence를 검증한다.

승인된 수용 기준 후보

  • 소유자의 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-hub Discovery 범위만 승인하며 구현 완료, lifecycle closure, main 반영, push, PR, 배포 또는 release를 의미하지 않는다.

Out of Scope

Canonical 경계

  • 위 승인 원문 전체의 Out of Scope와 Authority boundary를 그대로 적용한다.
  • 특히 trex-web 코드·타입·화면·테스트, /v1/strategies UI 적응, 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은 없다.