Files
portfolio-hub/docs/batches/bat-010_20260513_daemon-model-implementation/verification.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

10 KiB

BAT-010 Verification

  • Status: completed
  • Batch: BAT-010

Test Suite Results

7 failed, 128 passed in 4.88s
  • 7 failures: all pre-existing in tests/test_cli.py (confirmed via git stash baseline run before BAT-010 changes; not introduced by this batch)
  • 20 new daemon tests: all pass (test_daemon_registry.py: 12, test_daemon_assignment.py: 8)
  • All prior tests in test_auth.py, test_v1.py, test_portfolios.py, test_strategy_sets.py: pass

Acceptance Mapping

REQ-009: v1 API DB 스키마 migration (daemon schema 포함)

  • AC-1: daemons 테이블이 신규 생성된다.
    • Evidence: migrations/versions/20260513_0016_daemon_model.py — op.execute("CREATE TABLE IF NOT EXISTS daemons ..."); app/repositories/postgres.py ensure_schema() DDL guard
  • AC-2: auth_tokens.daemon_id TEXT 컬럼이 추가된다.
    • Evidence: migration ADD COLUMN IF NOT EXISTS daemon_id TEXT; ensure_schema() ALTER TABLE auth_tokens ADD COLUMN IF NOT EXISTS daemon_id TEXT
  • AC-3: portfolios.managed_by_daemon_id TEXT 컬럼이 추가된다.
    • Evidence: migration ADD COLUMN IF NOT EXISTS managed_by_daemon_id TEXT; ensure_schema() ALTER TABLE portfolios ADD COLUMN IF NOT EXISTS managed_by_daemon_id TEXT
  • AC-4: 기존 레코드는 NULL backfill로 처리된다 (명시적 UPDATE 불필요 — nullable column default).
    • Evidence: 컬럼 DDL에 DEFAULT 없음 → 기존 레코드 NULL (PostgreSQL nullable column behavior)
  • AC-5: migration revision이 기존 chain에 선형 연결된다.
    • Evidence: down_revision = "20260511_0015" in 20260513_0016_daemon_model.py

REQ-023: Daemon Registry 관리 경로

  • AC-1: pfhub daemon create <slug> --user <username> 가 daemon을 생성하고 daemon_id를 반환한다.
    • Evidence: app/cli/commands/daemon.py create_daemon(); tests/test_daemon_registry.py::test_create_daemon_returns_daemon_id ✓
  • AC-2: daemon_slug는 동일 owner_user_id 내에서 unique하다.
    • Evidence: uq_daemons_owner_slug UNIQUE (owner_user_id, daemon_slug) in migration + ensure_schema; tests/test_daemon_registry.py::test_create_daemon_duplicate_slug_raises ✓
  • AC-3: pfhub daemon list --user <username> 가 해당 user 소유 daemon만 반환한다.
    • Evidence: app/repositories/postgres.py list_daemons_for_user(); tests/test_daemon_registry.py::test_list_daemons_scoped_to_user ✓
  • AC-4: pfhub daemon update <daemon_id> --display-name <name> 가 display_name을 업데이트한다.
    • Evidence: app/cli/commands/daemon.py update_daemon(); tests/test_daemon_registry.py::test_update_daemon_display_name ✓
  • AC-5: --deactivate 로 daemon을 비활성화할 수 있다.
    • Evidence: app/cli/commands/daemon.py update_daemon --deactivate option → update_daemon(daemon_id, is_active=False)

REQ-005: service-token 발급 daemon binding

  • AC-1: pfhub service-token issue <name> --daemon-id <daemon_id> 가 daemon_id를 token에 binding한다.
    • Evidence: app/cli/commands/service_tokens.py issue() --daemon-id option; app/services/auth_service.py issue_service_token(daemon_id=...); tests/test_daemon_registry.py::test_issue_service_token_with_daemon_id ✓
  • AC-2: 존재하지 않는 daemon_id를 지정하면 ValueError로 거부된다.
    • Evidence: AuthService.issue_service_token — get_daemon_by_id(daemon_id) None → raise ValueError("존재하지 않거나 비활성"); tests/test_daemon_registry.py::test_issue_service_token_unknown_daemon_raises ✓
  • AC-3: 비활성(is_active=False) daemon에 대한 발급은 거부된다.
    • Evidence: AuthService.issue_service_token — not daemon.is_active → raise ValueError; tests/test_daemon_registry.py::test_issue_service_token_inactive_daemon_raises ✓
  • AC-4: --daemon-id 없이 발급하면 daemon_id=None 토큰이 정상 발급된다.
    • Evidence: tests/test_daemon_registry.py::test_issue_service_token_without_daemon_id_succeeds ✓

REQ-012: service-token rotate daemon_id 불변식

  • AC-1: pfhub service-token rotate <name> 시 daemon_id가 유지된다.
    • Evidence: app/repositories/postgres.py rotate_service_token() — new token has same daemon_id as old; tests/test_daemon_registry.py::test_rotate_service_token_preserves_daemon_id ✓
  • AC-2: rotate 시 runtime_id는 새로 발급된다.
    • Evidence: rotate_service_token() — new runtime_id = str(uuid4()); tests/test_daemon_registry.py::test_rotate_service_token_preserves_daemon_id — asserts rotated.auth_token.runtime_id != original_runtime_id ✓
  • AC-3: rotate 시 raw token 값이 변경된다.
    • Evidence: tests/test_daemon_registry.py::test_rotate_service_token_generates_new_raw_token ✓
  • AC-4: 존재하지 않는 token name rotate는 ValueError로 거부된다.
    • Evidence: tests/test_daemon_registry.py::test_rotate_nonexistent_token_raises ✓

REQ-006: GET /v1/assets daemon-scoped

  • AC-1: daemon_id=NULL 토큰으로 GET /v1/assets 호출 시 HTTP 403 반환.
    • Evidence: app/api/v1_assets.py — if auth.daemon_id is None: raise HTTPException(403, ...); tests/test_daemon_assignment.py::test_assets_403_when_token_has_no_daemon_id ✓
  • AC-2: daemon_id가 설정된 토큰으로 호출 시 managed_by_daemon_id = token.daemon_id 필터가 적용된 asset만 반환.
    • Evidence: app/api/v1_assets.py — list_portfolios_with_strategies(managed_by_daemon_id=auth.daemon_id); tests/test_daemon_assignment.py::test_assets_200_with_daemon_scoped_filter ✓ (mock assertion on called args)

REQ-007: GET /v1/user_info daemon_id 필드

  • AC-1: 응답에 daemon_id 필드가 포함된다 (bound token).
    • Evidence: app/schemas/v1.py UserInfoResponse.daemon_id; app/api/v1_user_info.py — daemon_id=auth.daemon_id; tests/test_daemon_assignment.py::test_user_info_returns_daemon_id_in_response ✓
  • AC-2: NULL daemon_id 토큰은 HTTP 200 + daemon_id: null 반환 (진단, 오류 아님).
    • Evidence: app/api/v1_user_info.py — no guard on daemon_id; tests/test_daemon_assignment.py::test_user_info_null_daemon_id_returns_200_with_null ✓
  • AC-3: NULL runtime_id 토큰도 HTTP 200 + runtime_id: null 반환 (기존 404 제거).
    • Evidence: app/api/v1_user_info.py — 404 guard removed; tests/test_v1.py::test_v1_user_info_null_runtime_id_returns_200_with_null ✓

REQ-008: POST /v1/assets/{asset_id}/ledger/trades daemon assignment authorization

  • AC-1: daemon_id=NULL 토큰으로 호출 시 HTTP 403.
    • Evidence: app/api/v1_ledger.py — check 1: if auth.daemon_id is None: raise HTTPException(403, ...); tests/test_daemon_assignment.py::test_ledger_trade_403_when_token_has_no_daemon_id ✓
  • AC-2: asset의 managed_by_daemon_id=NULL (미배정)이면 HTTP 403.
    • Evidence: app/api/v1_ledger.py — check 2: if portfolio.managed_by_daemon_id is None: raise HTTPException(403, ...); tests/test_daemon_assignment.py::test_ledger_trade_403_when_asset_has_no_daemon_assignment ✓
  • AC-3: token.daemon_id ≠ asset.managed_by_daemon_id (mismatch)이면 HTTP 403.
    • Evidence: app/api/v1_ledger.py — check 3: if portfolio.managed_by_daemon_id != auth.daemon_id: raise HTTPException(403, ...); tests/test_daemon_assignment.py::test_ledger_trade_403_when_daemon_mismatch ✓
  • AC-4: token.daemon_id == asset.managed_by_daemon_id이면 HTTP 201 + status="new".
    • Evidence: tests/test_daemon_assignment.py::test_ledger_trade_201_when_daemon_matches ✓

REQ-011: baseline docs daemon-aware 갱신

  • AC-1: docs/interface-contract.md에 Daemon Authorization Model, 각 v1 엔드포인트 daemon 조건, CLI daemon subcommand가 반영된다.
    • Evidence: docs/interface-contract.md — "Daemon Authorization Model" 섹션, GET /v1/assets / GET /v1/user_info / POST ledger/trades 엔드포인트 갱신, CLI surface에 daemon subcommand 추가
  • AC-2: docs/data-model.md에 Daemon entity, portfolios.managed_by_daemon_id, auth_tokens.daemon_id가 문서화된다.
    • Evidence: docs/data-model.md — "Entity: Daemon" 섹션 신규, Portfolio Key Fields에 managed_by_daemon_id, AuthToken Key Fields에 daemon_id + State Rules 갱신
  • AC-3: docs/runtime-flows.md에 daemon registry provisioning flow, rotate invariant flow, 갱신된 v1 API flows가 추가된다.
    • Evidence: docs/runtime-flows.md — "Daemon Registry — Create and Bind Service Token", "Service-Token Rotate — daemon_id Invariant" 신규; GET /v1/assets / GET /v1/user_info / POST ledger/trades flows 갱신

REQ-028: README.md daemon provisioning runbook

  • AC-1: README에 daemon 등록, service-token 바인딩, asset 할당 절차가 포함된다.
    • Evidence: README.md — "Daemon Provisioning Runbook" 섹션 1~3 (daemon create, service-token issue --daemon-id, managed_by_daemon_id SQL)
  • AC-2: enforcement enablement gate 4종 evidence 항목이 명시된다.
    • Evidence: README.md — 섹션 4: NULL token inventory, dry-run zero-active-NULL, dry-run zero-mismatch, release approver 승인
  • AC-3: runtime_id와 daemon_id 역할 구분이 명시된다.
    • Evidence: README.md — "runtime_id vs daemon_id" 섹션

Evidence

Test

  • pytest tests/ -q 결과: 128 passed, 7 failed (2026-05-13)
  • 7 failures: tests/test_cli.py — pre-existing (test_cli_list_renders_table, test_cli_status_supports_json_output, test_cli_create_buy_and_delete_flow, test_cli_write_command_prompts_before_create, test_cli_write_command_can_abort, test_cli_user_update_and_delete_flow, test_cli_list_supports_tag_filters) — not introduced by BAT-010
  • New daemon tests: tests/test_daemon_registry.py (12), tests/test_daemon_assignment.py (8) — all pass

Manual

  • 실제 TimescaleDB 인스턴스에 대한 통합 테스트: 미수행 (환경 미구성 — 잔여 리스크로 기록)
  • ensure_schema() 및 migration 20260513_0016 DDL: IF NOT EXISTS guard 확인 (코드 리뷰)

Logs

  • N/A (unit test 기반 검증; DB integration 미수행)

Result

  • Ready for Release: true
  • Blocking Issues: 없음
  • Notes:
    • Pre-existing 7 test failures in test_cli.py는 이 batch scope 밖이며 별도 배치에서 처리한다.
    • 프로덕션 enforcement 활성화는 배포 전 README 섹션 4의 4종 evidence 확보 후 수행한다.
    • migration 20260513_0016 실제 DB 적용 검증은 배포 시 수행한다.