Files
portfolio-hub/docs/runtime-flows.md
T
genichin e6b6a8715b feat: implement daemon model with registry and single-asset assignment (BAT-010, REQ-023/028)
Core Implementation (BAT-010):
- Add daemon registry schema and CRUD operations (req-023)
- Implement single-daemon asset assignment constraint (one asset per daemon)
- Add daemon_id field to service_token for daemon identification
- Add daemon.py CLI module for daemon management commands
- Add 20260513_0016_daemon_model.py migration for daemon tables

API Changes:
- GET /v1/assets: include daemon_id in AssetResponse
- POST /v1/assets/{asset_id}/ledger/trades: require daemon token binding
- GET /v1/user_info: include daemon metadata
- Enhanced service-token rotate to preserve daemon bindings

CLI Commands:
- pfhub daemon register --name <name> --runtime-id <runtime-id>
- pfhub daemon list
- pfhub daemon get <daemon-id>
- pfhub daemon revoke <daemon-id>

Requirements (REQ-023, REQ-028):
- REQ-023: Daemon Registry and Service Token Daemon Binding (Interface, Approved)
  * AC-1: daemon CRUD operations
  * AC-2: service token daemon binding for POST /v1/assets/{asset_id}/ledger/trades
  * AC-3: single daemon per asset (exact-one assignment model)
- REQ-028: README.md Daemon Provisioning Runbook update (Documentation, Approved)

Documentation:
- Add dcy-006 Discovery for daemon-id and single-daemon asset assignment
- Update README.md with Daemon Provisioning Runbook section
- Add BAT-010 batch documentation with planning, implementation, verification
- Update rel-006 release document (daemon model implementation)
- Update data-model.md with daemon and daemon_asset_assignment tables
- Update interface-contract.md with daemon registry APIs
- Update runtime-flows.md with daemon lifecycle flows

Breaking Changes:
- REQ-005~009, REQ-011~012 status reverted to Approved due to revised contract
  * daemon_id field addition and exact-one asset assignment requirement
  * Existing Implemented evidence invalidated by contract revision
  * BAT-001/002/008 re-verification required

Testing:
- Add test_daemon_registry.py for daemon CRUD operations
- Add test_daemon_assignment.py for single-asset assignment constraints
- Update test_auth.py, test_cli.py, test_v1.py for daemon support
- Verify daemon token binding in trade endpoint authorization

Impact:
- Database: 2 new tables (daemons, daemon_asset_assignments)
- Migration: 20260513_0016 creates daemon model schema
- CLI: 4 new daemon subcommands
- API: daemon_id propagation through v1 endpoints
- Verification: BAT-001/002/008 re-verification gates next release
2026-05-13 23:07:24 +00:00

15 KiB

Portfolio Hub Runtime Flows

  • Baseline Mode: observed
  • Baseline Seed: .stagepilot/bootstrap/baseline.yaml
  • Status: confirmed
  • Owner: repository-maintainers
  • Last Updated (KST): 2026-05-13 00:00
  • Source Discovery / Batch: bootstrap-baseline, bat-001, bat-006, bat-007, bat-008, bat-010
  • Primary Runtime: mixed

Project Summary

  • PortfolioHub exposes portfolio management and trading ledger workflows through a FastAPI service and a companion CLI.

Purpose

  • This document is the current approved execution-flow baseline for the project.
  • It is a cross-cutting reference document for Discovery, REQ, Batch Design, Batch Verification, and implementation updates.

Scope

  • Covers representative command and runtime flows.
  • Does not duplicate full acceptance criteria or low-level implementation details.

Covered Entry Points

  • uvicorn app.main:app :: serve the authenticated HTTP API, OpenAPI docs, and startup bootstrap hooks
  • pfhub :: run operational CLI commands for health, users, portfolios, strategies, service tokens, and daemon registry
  • alembic upgrade head :: apply ordered database schema migrations for persistent environments

Shared Components

  • app/core/config.py Settings loading and environment normalization
  • app/core/auth.py permission guards and auth context resolution
  • app/services/* business logic for auth, users, portfolios, and strategy sets
  • app/repositories/factory.py repository selection and Postgres config construction
  • app/repositories/postgres.py Postgres persistence, schema bootstrap, and query execution

Flow: HTTP API Request Handling

  1. Uvicorn loads app.main:app, which creates the FastAPI application from create_app().
  2. create_app() loads Settings, registers middleware and routers, and wires exception handlers.
  3. During lifespan startup, the app optionally bootstraps an initial admin user when DB mode and bootstrap credentials are configured.
  4. Incoming requests pass through auth and permission dependencies before services mutate state or query persistence.
  5. Services call the repository factory, which returns PostgresPortfolioRepository only when PORTFOLIOHUB_DB_ENABLED=true.
  6. Routes serialize domain results into app/schemas response models or return HTTP 204 for deletion flows.

Flow: CLI Operational Command Execution

  1. The pfhub entrypoint builds a Typer application and injects Settings, console output, and a repository factory into AppContext.
  2. Command groups parse arguments, confirmations, and output format selection before invoking the relevant service or repository methods.
  3. Database setup commands use the shared settings layer to target the configured PostgreSQL or TimescaleDB instance.
  4. Read commands print table or JSON output, while write commands require explicit confirmation unless --force is supplied.
  5. Command failures surface as validation errors, feature-not-ready errors, or persistence and auth exceptions.

Flow: CLI Portfolio Command — Name+User Resolution

  1. All portfolio mutation commands (status, create, update, buy, sell, invest, transfer, reset, delete) accept a <name> positional argument and --user <username> option.
  2. _resolve_portfolio_id(repository, name, user) is called before any mutation: a. Calls get_user_by_username(user) → raises ValueError if user not found. b. Calls get_portfolio_by_name_and_user(owner_user_id, name) → raises ValueError if portfolio not found. c. Returns portfolio.portfolio_id (UUID) for downstream use.
  3. create command takes <name> as the portfolio identifier; portfolio_id (UUID) is server-generated and returned in the response.
  4. update --new-name <new_name> renames the portfolio; --force is required to confirm the rename.
  5. comment and tag sub-commands continue to accept <portfolio_id> (UUID) directly; they are outside the name+user resolution flow.

Flow Constraints

  • Persistence-dependent HTTP and CLI flows require PORTFOLIOHUB_DB_ENABLED=true plus valid TimescaleDB connection settings.
  • Permission levels read, trade-write, and admin must map consistently across API guards, issued tokens, and CLI mutations.
  • Request validation and auth resolution should happen before persistence mutation or response rendering.
  • The health endpoint remains unauthenticated so infrastructure can probe readiness without a token.

Flow: v1 API — Service-Token Authentication

  1. v1 endpoint receives Bearer token from Authorization header.
  2. require_read resolves the token: checks env tokens first, then DB lookup via get_token_principal().
  3. If DB token: validates token_source == "db" and token_type == "service", else returns HTTP 403.
  4. AuthContext is populated with user_id, token_type, permission, and runtime_id.

Flow: v1 API — GET /v1/assets

  1. Service-token auth required (see above).
  2. Daemon-id guard: if auth.daemon_id is None → HTTP 403 ("daemon_id not assigned to this token"). NULL tokens are fail-closed.
  3. list_portfolios_with_strategies() fetches only the token owner's portfolios scoped to managed_by_daemon_id=auth.daemon_id; owner_user_id=auth.user_id prevents cross-user leakage.
  4. Optional ?status=<value> query param filters portfolios by status field; include_disabled=False by default.
  5. For portfolios with active_strategy_id, get_strategy_set() fetches config and JSON-parses strategy_params; failures fall back to {}.
  6. Returns AssetsResponse with total and assets[] using field names: cash, quantity, broker_type, strategy_params.
  7. asset_id in each asset item equals portfolio.portfolio_id (server-generated UUID after BAT-006 migration).

Flow: v1 API — GET /v1/user_info

  1. Service-token auth required. Available to all service tokens including unbound (daemon_id=NULL); no daemon-id guard.
  2. get_user() fetches user record by user_id.
  3. list_broker_credentials() fetches all broker credential rows for the user.
  4. For each credential, Fernet-decrypts encrypted_payload using CREDENTIAL_ENCRYPTION_KEY; decryption failures log a warning and return credential_payload: null.
  5. Returns UserInfoResponse with trading_enabled (from is_active), disabled_reason, brokers[], runtime_id (null if unbound), and daemon_id (null for legacy/unbound tokens — diagnostic, not an error).

Flow: v1 API — POST /v1/assets/{asset_id}/ledger/trades

  1. Service-token auth required with trade-write permission.
  2. Validates asset existence via repo.get(asset_id); HTTP 404 if missing.
  3. Daemon assignment authorization (fail-closed, three checks in order): a. auth.daemon_id is None → HTTP 403 ("daemon_id not assigned to this token"). b. portfolio.managed_by_daemon_id is None → HTTP 403 ("Asset is not assigned to any daemon"). c. portfolio.managed_by_daemon_id != auth.daemon_id → HTTP 403 ("Token daemon does not match asset daemon assignment").
  4. Calls insert_ledger_trade() with idempotency key (asset_id, order_id, execution_id).
  5. If new record: returns HTTP 201 with status="new".
  6. If duplicate key with identical payload: returns HTTP 200 with status="duplicate".
  7. If duplicate key with different payload: returns HTTP 409 with conflict detail.

Flow: v1 API — POST /auth/service-tokens

  1. Admin token auth required (require_admin dependency).
  2. Validates request body: name, username, permission, expires_in_days / no_expiry.
  3. Calls AuthService.issue_service_token() which creates a new DB token with runtime_id.
  4. Returns HTTP 201 with token (raw value), runtime_id, and auth_token metadata.

Flow: Daemon Registry — Create and Bind Service Token

  1. Operator creates a daemon via pfhub daemon create <daemon_slug> --user <username> (optionally --display-name <name>).
    • AuthService is not involved; the repository creates the Daemon record directly with owner_user_id resolved from username.
    • Returns daemon.id (server-generated UUID), daemon_slug, is_active=True.
  2. Operator issues a service token bound to the daemon via pfhub service-token issue <name> --user <username> --daemon-id <daemon_id>.
    • AuthService.issue_service_token() validates: daemon exists, is_active=True, and owner_user_id matches the token's user.
    • Unknown or inactive daemon → ValueError ("존재하지 않거나 비활성").
    • create_auth_token() persists the token with daemon_id set.
  3. Operator assigns an asset to the daemon via portfolio update (managed_by_daemon_id column set).
  4. GET /v1/assets with the bound token returns only assets where portfolios.managed_by_daemon_id = token.daemon_id.
  5. POST /v1/assets/{asset_id}/ledger/trades with the bound token passes daemon assignment authorization and records the trade.

Flow: Service-Token Rotate — daemon_id Invariant

  1. Operator calls pfhub service-token rotate <name> --force.
  2. AuthService.rotate_service_token(name) delegates to repository.rotate_service_token(name).
  3. Repository looks up the existing token by name; raises ValueError if not found.
  4. Old token is revoked (revoked_at = NOW()).
  5. New token row is created with:
    • New raw token value (generate_token()), new token_hash, new token_prefix.
    • New runtime_id (uuid4) — per-token, changes on every rotate.
    • Same daemon_id as the old token — stable across rotates (daemon_id invariant).
    • Same user_id, name, permission, token_type, expires_at.
  6. Returns raw token (shown once) and new auth_token metadata including new runtime_id.
  7. GET /v1/user_info called with the new token returns the same daemon_id but a new runtime_id.

Flow: Application Startup Validation

  1. lifespan startup hook runs _bootstrap_initial_user() if DB enabled and bootstrap credentials configured.
  2. If db_enabled=True, calls settings.require_credential_encryption_key().
  3. If CREDENTIAL_ENCRYPTION_KEY is not configured, raises RuntimeError and aborts startup.

Flow: Local DB Initialization and Validation

Scenario A — Fresh DB (DB 미존재 상태)

  1. docker compose -f docker-compose.local-test.yml up -d — TimescaleDB 컨테이너 시작.
  2. 컨테이너 준비 확인: docker compose -f docker-compose.local-test.yml ps → portfolio-hub-timescaledb 상태가 healthy 인지 확인.
  3. pfhub_DB_ENABLED=true pfhub init-db — 대상 DB 생성 + Alembic upgrade head 실행.
    • 기대 출력: database: portfolio_hub, created: True, schema_initialized: True, migration_applied: head
    • pass 기준: exit code 0, 위 필드 모두 출력
    • fail 기준: connection refused, permission denied, Alembic revision 오류
  4. pfhub_DB_ENABLED=true pfhub health-check — DB ping 성공 확인.
    • 기대 출력: db_enabled: True, repository: PostgresPortfolioRepository
    • pass 기준: exit code 0, db_enabled: True
  5. Smoke flow: 사용자 생성 → portfolio 생성 → portfolio list --user <username> 확인.
    • pass 기준: 생성된 portfolio가 list 출력에 포함됨

Scenario B — runtime-precreated schema (ensure_schema() 선행)

  1. DB 컨테이너 시작 (Scenario A 1~2 동일).
  2. pfhub_DB_ENABLED=true uvicorn app.main:app — App 기동. ensure_schema()가 핵심 테이블 생성.
  3. pfhub_DB_ENABLED=true pfhub migrate-db — bootstrap_or_upgrade_to_head() 실행.
    • 내부 동작: alembic_version 테이블 없음 → 기존 핵심 테이블(portfolios 등) 존재 → legacy stamp → head로 stamp + 미보유 객체(broker_credentials, ledger_trades, runtime_id 등) upgrade
    • 기대 출력: migration_applied: head
    • pass 기준: exit code 0, 이후 pfhub health-check exit code 0
  4. pfhub_DB_ENABLED=true pfhub health-check 재확인.

Repeatable Run Criteria

  • 모든 시나리오는 컨테이너 볼륨 초기화(docker compose down -v) 후 재실행 가능.
  • 출력 형식: pfhub --output json <command> 로 JSON 출력 수집 시 필드명이 고정됨.
  • 로그 수집 위치: docker compose logs portfolio-hub-timescaledb — DB 측 오류 시 참조.
  • Evidence 형식: exit code + 주요 필드 값 스크린샷 또는 텍스트 캡처 → batch verification.md에 기록.

Flow: Alembic vs ensure_schema() Schema Ownership Boundary

ensure_schema() (postgres.py)와 Alembic revision 양쪽이 다루는 객체가 존재한다. 충돌은 bootstrap_or_upgrade_to_head()의 legacy stamp 경로 + 각 revision의 IF NOT EXISTS guard로 처리된다.

경계 규칙

  1. ensure_schema() 선행 가능 객체: users, auth_tokens, strategy_sets, portfolios, portfolio_ledger, portfolio_comments, portfolio_tags + 관련 인덱스. 모두 IF NOT EXISTS로 보호됨. BAT-010: auth_tokens.daemon_id, portfolios.managed_by_daemon_id, daemons 테이블도 ensure_schema()에 ADD/CREATE IF NOT EXISTS guard로 추가됨.
  2. Alembic만 소유 (ensure_schema()에 없음): portfolios.market, auth_tokens.runtime_id, idx_auth_tokens_runtime_id_unique, broker_credentials, ledger_trades. ensure_schema()만 실행한 환경에서 v1 API 사용 불가.
  3. BAT-010 migration 20260513_0016: daemons 테이블, auth_tokens.daemon_id, portfolios.managed_by_daemon_id 추가. down_revision=20260511_0015. ensure_schema() 선행 환경에서도 migration 적용 시 IF NOT EXISTS guard로 safe.
  4. 허용 실행 순서: init-db 선행 권장. 불가피하게 ensure_schema() 선행 시, 반드시 migrate-db 후속 실행.
  5. 금지: alembic_version 테이블 수동 삭제 후 migrate-db 재실행 (legacy stamp 오판).

Current Gaps / Planned Changes

  • End-to-end validation against a real PostgreSQL or TimescaleDB deployment is still pending.
  • Redis and Loki are configured as supporting infrastructure but are not yet represented as explicit runtime flows.
  • Broker credential write path (upsert) is available only via CLI; no HTTP API endpoint for credential registration exists yet.

Change Log

  • 2026-05-13 KST: bat-010 — REQ-006/007/008/009/023/005/012: updated GET /v1/assets flow (daemon-id guard, daemon-scoped filter); updated GET /v1/user_info flow (daemon_id diagnostic, NULL → 200+null); updated POST ledger/trades flow (3-step daemon assignment auth, fail-closed); added "Daemon Registry — Create and Bind Service Token" flow; added "Service-Token Rotate — daemon_id Invariant" flow; updated Alembic/ensure_schema boundary (BAT-010 migration 20260513_0016); updated Covered Entry Points (daemon subcommand).
  • 2026-05-11 KST: bat-008 — REQ-011 AC-3 confirmed: v1 API flows and runtime_id issuance flow already present from bat-001; Source Discovery/Batch header updated.
  • 2026-05-11 KST: bat-007 — Added "Local DB Initialization and Validation" flow (Scenario A/B, repeatable run criteria); added "Alembic vs ensure_schema() Schema Ownership Boundary" flow.
  • 2026-05-11 KST: bat-006 — Added CLI portfolio name+user resolution flow; updated GET /v1/assets flow to note asset_id is UUID after BAT-006 migration.
  • 2026-05-01 KST: bat-001 — Added v1 API flows (service-token auth, GET /v1/assets, GET /v1/user_info, POST ledger/trades, POST /auth/service-tokens), startup validation flow.
  • 2026-04-30 KST: Bootstrap baseline created from repository observation for initial StagePilot adoption.