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
10 KiB
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.pyensure_schema() DDL guard
- Evidence:
- AC-2:
auth_tokens.daemon_idTEXT 컬럼이 추가된다.- Evidence: migration
ADD COLUMN IF NOT EXISTS daemon_id TEXT; ensure_schema()ALTER TABLE auth_tokens ADD COLUMN IF NOT EXISTS daemon_id TEXT
- Evidence: migration
- AC-3:
portfolios.managed_by_daemon_idTEXT 컬럼이 추가된다.- 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
- Evidence: migration
- 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
- Evidence:
REQ-023: Daemon Registry 관리 경로
- AC-1:
pfhub daemon create <slug> --user <username>가 daemon을 생성하고 daemon_id를 반환한다.- Evidence:
app/cli/commands/daemon.pycreate_daemon();tests/test_daemon_registry.py::test_create_daemon_returns_daemon_id✓
- Evidence:
- AC-2: daemon_slug는 동일 owner_user_id 내에서 unique하다.
- Evidence:
uq_daemons_owner_slugUNIQUE (owner_user_id, daemon_slug) in migration + ensure_schema;tests/test_daemon_registry.py::test_create_daemon_duplicate_slug_raises✓
- Evidence:
- AC-3:
pfhub daemon list --user <username>가 해당 user 소유 daemon만 반환한다.- Evidence:
app/repositories/postgres.pylist_daemons_for_user();tests/test_daemon_registry.py::test_list_daemons_scoped_to_user✓
- Evidence:
- AC-4:
pfhub daemon update <daemon_id> --display-name <name>가 display_name을 업데이트한다.- Evidence:
app/cli/commands/daemon.pyupdate_daemon();tests/test_daemon_registry.py::test_update_daemon_display_name✓
- Evidence:
- AC-5:
--deactivate로 daemon을 비활성화할 수 있다.- Evidence:
app/cli/commands/daemon.pyupdate_daemon--deactivateoption →update_daemon(daemon_id, is_active=False)
- Evidence:
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.pyissue()--daemon-idoption;app/services/auth_service.pyissue_service_token(daemon_id=...);tests/test_daemon_registry.py::test_issue_service_token_with_daemon_id✓
- Evidence:
- 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✓
- Evidence:
- 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✓
- Evidence:
- AC-4:
--daemon-id없이 발급하면 daemon_id=None 토큰이 정상 발급된다.- Evidence:
tests/test_daemon_registry.py::test_issue_service_token_without_daemon_id_succeeds✓
- Evidence:
REQ-012: service-token rotate daemon_id 불변식
- AC-1:
pfhub service-token rotate <name>시 daemon_id가 유지된다.- Evidence:
app/repositories/postgres.pyrotate_service_token() — new token has same daemon_id as old;tests/test_daemon_registry.py::test_rotate_service_token_preserves_daemon_id✓
- Evidence:
- 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— assertsrotated.auth_token.runtime_id != original_runtime_id✓
- Evidence: rotate_service_token() — new
- AC-3: rotate 시 raw token 값이 변경된다.
- Evidence:
tests/test_daemon_registry.py::test_rotate_service_token_generates_new_raw_token✓
- Evidence:
- AC-4: 존재하지 않는 token name rotate는 ValueError로 거부된다.
- Evidence:
tests/test_daemon_registry.py::test_rotate_nonexistent_token_raises✓
- Evidence:
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✓
- Evidence:
- 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)
- Evidence:
REQ-007: GET /v1/user_info daemon_id 필드
- AC-1: 응답에
daemon_id필드가 포함된다 (bound token).- Evidence:
app/schemas/v1.pyUserInfoResponse.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✓
- Evidence:
- 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✓
- Evidence:
- 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✓
- Evidence:
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✓
- Evidence:
- 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✓
- Evidence:
- 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✓
- Evidence:
- 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✓
- Evidence:
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 추가
- Evidence:
- 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 갱신
- Evidence:
- 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 갱신
- Evidence:
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)
- Evidence:
- 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 승인
- Evidence:
- AC-3: runtime_id와 daemon_id 역할 구분이 명시된다.
- Evidence:
README.md— "runtime_id vs daemon_id" 섹션
- Evidence:
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 EXISTSguard 확인 (코드 리뷰)
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 적용 검증은 배포 시 수행한다.