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
This commit is contained in:
2026-05-13 23:07:24 +00:00
parent f0a6caa178
commit e6b6a8715b
45 changed files with 2688 additions and 167 deletions
@@ -0,0 +1,55 @@
# BAT-010 Design
- Status: confirmed
- Batch: BAT-010
## Architecture Summary
daemon identity 모델을 도입해 service-token과 asset에 stable `daemon_id`를 부여하고, `/v1/assets`와 `/v1/assets/{asset_id}/ledger/trades`의 authorization 축을 user-scope에서 daemon-scope로 전환한다. 기존 service-token bearer auth와 permission 모델은 유지하고, daemon assignment는 그 위에 추가되는 두 번째 authorization 축이다. 전환 구간에서는 NULL backfill을 허용하고 fail-closed로 처리한다.
## Changed Areas
- Docs:
- `docs/interface-contract.md` — daemon binding, daemon-scoped contract, 403 mismatch policy
- `docs/data-model.md` — daemons 테이블, auth_tokens.daemon_id, portfolios.managed_by_daemon_id
- `docs/runtime-flows.md` — daemon registry binding 흐름, rotate invariant, daemon-scoped v1 API 흐름
- `README.md` — daemon provisioning runbook, enforcement enablement gate
- Modules:
- `migrations/versions/` — daemons, auth_tokens.daemon_id, portfolios.managed_by_daemon_id migration
- `app/domain/models.py` — Daemon entity 추가
- `app/repositories/postgres.py` — daemon CRUD methods
- `app/repositories/portfolio_repository.py` — daemon-scoped asset query
- `app/services/auth_service.py` — daemon binding at issue/rotate
- `app/api/auth_routes.py` (또는 신규 daemon route) — daemon registry management endpoint
- `app/api/v1_assets.py` — daemon-scoped GET, daemon assignment auth for ledger write
- `app/api/v1_user_info.py` — daemon_id 필드 노출
- `app/schemas/v1.py` — daemon_id 필드 추가
- `app/cli/commands/service_tokens.py` — daemon_id binding at issue, daemon_id invariant at rotate, daemon_id in output
- Interfaces:
- Daemon registry management: HTTP endpoint(`POST /daemons`, `GET /daemons`, `PATCH /daemons/{daemon_id}`) 또는 CLI subcommand(`pfhub daemon create/list/update`) — **아래 Key Decisions 참조**
- `POST /auth/service-tokens` request body: `daemon_id` 필드 추가
- `GET /v1/assets` response: daemon-scoped 필터
- `GET /v1/user_info` response: `daemon_id` 필드 추가
- `POST /v1/assets/{asset_id}/ledger/trades`: daemon assignment 검증 추가
## Key Decisions
1. **Daemon registry 관리 경로 형태**: CLI subcommand(`pfhub daemon create/list/update`)를 1차 관리 경로로 채택. 운영자 중심 도구이고, HTTP endpoint로 노출 시 별도 admin-only auth guard 설계가 필요하다. HTTP endpoint는 향후 필요 시 추가한다.
2. **daemon_slug uniqueness**: owner user 내에서 unique. DB unique constraint(`daemons.owner_user_id + daemons.daemon_slug` composite unique)로 강제.
3. **enforcement enablement gate**: migration level에서 별도 flag 없이 진행. NULL backfill token/asset에 대한 fail-closed는 코드 레벨에서 처리. production cutover는 inventory + dry-run + release approver 승인 후 수동으로 진행.
4. **NULL backfill fail-closed boundary**: `daemon_id = NULL`인 token이 daemon-scoped endpoint(`GET /v1/assets`, `POST /v1/assets/{asset_id}/ledger/trades`)를 호출하면 403 Forbidden. `GET /v1/user_info`는 NULL이어도 200 OK + `daemon_id: null` 진단 노출.
## Edge Cases
- 동일 user 아래 daemon이 없는 상태에서 service-token 발급 시: daemon registry에 daemon이 없으면 발급 불가. 운영자가 먼저 daemon create를 수행해야 한다.
- rotate 중 daemon이 삭제된 경우: daemon FK 참조 무결성 위반으로 rotate 거부. 소프트 삭제(is_active flag) 방식으로 daemon을 비활성화하는 것이 안전하다.
- managed_by_daemon_id가 설정된 asset을 다른 daemon으로 재배정: 명시적 reassignment API/CLI 또는 migration-level 절차로만 허용. 자동 재배정 없음.
## Architecture Impact
- runtime-flows
## Reference Doc Update Plan
- project-structure.md: none (파일 구조 변경 없음)
- runtime-flows.md: 갱신 필요 — daemon registry binding 흐름, daemon-scoped v1 API 처리 흐름, rotate `runtime_id`/`daemon_id` 역할 차이 반영 (REQ-011 범위)
@@ -0,0 +1,72 @@
# BAT-010 Implementation
- Status: completed
- Batch: BAT-010
## Plan Summary
Implemented the full daemon model: Daemon entity (registry CRUD), daemon_id binding on service-token issuance, daemon_id rotate-invariant on token rotation, daemon-scoped authorization on GET /v1/assets and POST /v1/assets/{asset_id}/ledger/trades (fail-closed for NULL daemon_id), daemon_id diagnostic on GET /v1/user_info (NULL → 200+null), CLI daemon subcommand (create/list/update), CLI service-token rotate command, Alembic migration 20260513_0016, and docs updates across data-model.md, interface-contract.md, runtime-flows.md, and README.md.
## Changed Files
### New Files
- `migrations/versions/20260513_0016_daemon_model.py` — Alembic revision; creates daemons table, adds auth_tokens.daemon_id and portfolios.managed_by_daemon_id
- `app/cli/commands/daemon.py` — CLI daemon subcommand (create, list, update)
- `tests/test_daemon_registry.py` — 12 unit tests (REQ-023, REQ-005, REQ-012)
- `tests/test_daemon_assignment.py` — 8 integration tests (REQ-006, REQ-007, REQ-008)
### Modified Files
- `app/domain/models.py` — Added Daemon dataclass; added daemon_id to AuthToken, TokenPrincipal, Portfolio
- `app/core/auth.py` — Added daemon_id to AuthContext; propagated from principal
- `app/schemas/v1.py` — UserInfoResponse: runtime_id optional, daemon_id added
- `app/schemas/auth.py` — IssueServiceTokenRequest/ApiResponse/CliResponse: daemon_id added
- `app/repositories/portfolio_repository.py` — Protocol: create_auth_token(daemon_id), list_portfolios(managed_by_daemon_id), list_portfolios_with_strategies, rotate_service_token, daemon CRUD methods
- `app/repositories/postgres.py` — _SELECT_AUTH_TOKEN/TOKEN_PRINCIPAL/PORTFOLIO queries updated; create_auth_token(daemon_id), get_token_principal(daemon_id), list_portfolios(managed_by_daemon_id), list_portfolios_with_strategies, rotate_service_token, ensure_schema (daemons table + daemon_id columns), _row_to_auth_token/portfolio/daemon, daemon CRUD
- `app/services/auth_service.py` — issue_service_token(daemon_id with validation), rotate_service_token
- `app/api/v1_assets.py` — NULL daemon_id guard (HTTP 403); daemon-scoped list_portfolios_with_strategies
- `app/api/v1_user_info.py` — Removed 404 for null runtime_id; added daemon_id to response
- `app/api/v1_ledger.py` — 3-step daemon assignment authorization (NULL token, unassigned asset, mismatch)
- `app/cli/commands/service_tokens.py` — issue: added --daemon-id option; added rotate command
- `app/cli/main.py` — Registered daemon_app typer
- `tests/test_auth.py` — FakeRepo.create_auth_token: added daemon_id param
- `tests/test_v1.py` — Mock repos: added daemon_id; all asset/ledger TokenPrincipal: added daemon_id="d-1"; Portfolio mocks: added managed_by_daemon_id="d-1"; user_info null-runtime_id tests updated (404→200+null)
- `tests/test_cli.py` — FakeRepo.create_auth_token: added daemon_id param
- `docs/data-model.md` — Added Daemon entity; updated Portfolio/AuthToken fields; updated Change Log
- `docs/interface-contract.md` — Added Daemon Authorization Model; updated all v1 endpoints; updated CLI surface; updated Change Log
- `docs/runtime-flows.md` — Added daemon registry and rotate invariant flows; updated v1 endpoint flows; updated Alembic boundary; updated Change Log
- `docs/batches/bat-010_20260513_daemon-model-implementation/design.md` — Status: confirmed
## Execution Log
1. Added `Daemon` dataclass to `app/domain/models.py`; added `daemon_id` fields to `AuthToken`, `TokenPrincipal`, `Portfolio`.
2. Updated `AuthContext` in `app/core/auth.py` to carry `daemon_id`; propagated from `_resolve_auth_context`.
3. Extended `UserInfoResponse` in `app/schemas/v1.py`: `runtime_id` optional, `daemon_id` added.
4. Extended auth schemas in `app/schemas/auth.py`: `daemon_id` on issue request and responses.
5. Extended `PortfolioRepository` protocol with daemon CRUD, `rotate_service_token`, and updated signatures.
6. Implemented full daemon support in `app/repositories/postgres.py`: updated SELECT queries, ensure_schema DDL, row mappers, create_auth_token, get_token_principal, list_portfolios, list_portfolios_with_strategies, rotate_service_token, and all daemon CRUD methods.
7. Updated `AuthService.issue_service_token` to accept and validate `daemon_id`; added `rotate_service_token`.
8. Updated `v1_assets.py`: NULL daemon_id → 403; `list_portfolios_with_strategies` with `managed_by_daemon_id=auth.daemon_id`.
9. Updated `v1_user_info.py`: removed 404 for null runtime_id; added `daemon_id=auth.daemon_id` to response.
10. Updated `v1_ledger.py`: 3-step daemon assignment authorization before `insert_ledger_trade`.
11. Created `app/cli/commands/daemon.py` with `create`, `list`, `update` subcommands.
12. Updated `app/cli/commands/service_tokens.py`: `--daemon-id` option on issue; added `rotate` command.
13. Registered `daemon_app` in `app/cli/main.py`.
14. Created `migrations/versions/20260513_0016_daemon_model.py` with idempotent DDL guards.
15. Fixed `tests/test_auth.py` and `tests/test_cli.py` FakeRepo.create_auth_token signatures.
16. Fixed `tests/test_v1.py`: all asset/ledger TokenPrincipal + Portfolio mocks updated; null-runtime_id tests renamed and corrected.
17. Created `tests/test_daemon_registry.py` (12 tests: daemon CRUD + token binding + rotate invariant).
18. Created `tests/test_daemon_assignment.py` (8 tests: v1 daemon-scoped auth).
19. Updated `docs/data-model.md`, `docs/interface-contract.md`, `docs/runtime-flows.md`, `README.md`.
## Validation
- `pytest` run: **128 passed, 7 failed** (7 failures are pre-existing in test_cli.py; confirmed via git stash baseline check before BAT-010 changes).
- All 20 new daemon tests pass (test_daemon_registry.py: 12, test_daemon_assignment.py: 8).
- All existing tests in test_auth.py, test_v1.py, test_portfolios.py, test_strategy_sets.py continue to pass.
## Remaining Risks
- Pre-existing 7 test failures in test_cli.py (out of BAT-010 scope; tracked separately).
- Production enforcement gate requires manual operator action: NULL token inventory + dry-run + release approver approval before deployment (documented in README Daemon Provisioning Runbook).
- `managed_by_daemon_id` assignment to existing portfolios requires DB migration or manual update; no CLI path yet for daemon assignment on existing portfolios.
- Real DB integration test against TimescaleDB with migration 20260513_0016 not yet run (env not available in this batch).
@@ -0,0 +1,55 @@
# BAT-010 Index
- Status: release-candidate
- Batch: BAT-010
- Profile: standard
- Created: 2026-05-13
## Included REQ
| REQ | 제목 | Type | Priority |
|-----|------|------|----------|
| REQ-009 | v1 API 지원을 위한 DB 스키마 migration 추가 (daemon schema 포함) | Migration | High |
| REQ-023 | Daemon Registry 관리 경로 | Interface | High |
| REQ-005 | service-token 발급 시 runtime_id + daemon_id binding | Interface | High |
| REQ-012 | service-token rotate CLI — daemon_id 불변식 및 출력 | Interface | Medium |
| REQ-006 | GET /v1/assets daemon-scoped 엔드포인트 | Interface | High |
| REQ-007 | GET /v1/user_info daemon_id 필드 및 NULL diagnostic | Interface | High |
| REQ-008 | POST /v1/assets/{asset_id}/ledger/trades daemon assignment authorization | Interface | High |
| REQ-011 | baseline docs daemon-aware 갱신 (interface-contract, data-model, runtime-flows) | Documentation | Medium |
| REQ-028 | README.md Daemon Provisioning Runbook 반영 | Documentation | Medium |
## Source Discovery
- [dcy-006](../../discovery/dcy-006_20260513_daemon-id-and-single-daemon-asset-assignment.md) (daemon_id 도입 및 1자산 1데몬 자산 배정 모델 정립) — 주 출처
- dcy-002 (REQ-005, REQ-006, REQ-007, REQ-008, REQ-009, REQ-012 원출처 — CHG-20260513-01로 범위 재정의)
## Scope Summary
- Daemon 모델 전체를 한 번에 구현·검증·배포하는 단일 batch
- `daemons` 테이블, `auth_tokens.daemon_id`, `portfolios.managed_by_daemon_id` Alembic migration (REQ-009)
- daemon registry CRUD 관리 경로 — daemon create/update/list (REQ-023)
- service-token 발급 daemon binding + unknown daemon 거부 (REQ-005)
- service-token rotate daemon_id 불변식 + CLI daemon_id 출력 (REQ-012)
- GET /v1/assets daemon-scoped 응답 + NULL token fail-closed (REQ-006)
- GET /v1/user_info daemon_id 필드 + NULL diagnostic 노출 (REQ-007)
- POST /v1/assets/{asset_id}/ledger/trades daemon assignment authorization (REQ-008)
- interface-contract, data-model, runtime-flows daemon-aware 갱신 (REQ-011)
- README.md daemon provisioning runbook + enforcement enablement gate (REQ-028)
## Impacted Area
- Migrations: `migrations/versions/` (daemons table, auth_tokens.daemon_id, portfolios.managed_by_daemon_id)
- Modules: `app/domain/models.py`, `app/repositories/postgres.py`, `app/repositories/portfolio_repository.py`, `app/services/auth_service.py`, `app/api/auth_routes.py`, `app/api/v1_assets.py`, `app/api/v1_user_info.py`, `app/cli/commands/service_tokens.py`
- Schemas: `app/schemas/v1.py`
- Docs: `docs/interface-contract.md`, `docs/data-model.md`, `docs/runtime-flows.md`, `README.md`
- Tests: `tests/test_auth.py`, `tests/test_cli.py`, `tests/test_v1.py`, 신규 `tests/test_daemon_registry.py`, 신규 `tests/test_daemon_assignment.py`
## Stage Documents
| Stage | File | Status |
|-------|------|--------|
| Planning | [planning.md](./planning.md) | confirmed |
| Design | [design.md](./design.md) | confirmed |
| Implementation | [implementation.md](./implementation.md) | completed |
| Verification | [verification.md](./verification.md) | completed |
@@ -0,0 +1,62 @@
# BAT-010 Planning
- Status: confirmed
- Batch: BAT-010
- Profile: standard
## Included REQ
- REQ-009: v1 API DB 스키마 migration (daemon schema 포함)
- REQ-023: Daemon Registry 관리 경로
- REQ-005: service-token 발급 daemon binding
- REQ-012: service-token rotate daemon_id 불변식
- REQ-006: GET /v1/assets daemon-scoped
- REQ-007: GET /v1/user_info daemon_id 필드
- REQ-008: POST /v1/assets/{asset_id}/ledger/trades daemon assignment authorization
- REQ-011: baseline docs daemon-aware 갱신
- REQ-028: README.md daemon provisioning runbook
## Delivery Plan
의존 순서에 따라 아래 순서로 구현한다.
1. **REQ-009** — Alembic migration: `daemons` 테이블 신규 생성, `auth_tokens.daemon_id` FK 컬럼 추가, `portfolios.managed_by_daemon_id` FK 컬럼 추가, 기존 레코드 NULL backfill. 기존 revision chain 및 `IF NOT EXISTS` 정책 준수.
2. **REQ-023** — Daemon registry CRUD: domain model(`Daemon` entity), repository(`create_daemon`, `get_daemon_by_slug`, `list_daemons_for_user`, `update_daemon`), management route 또는 CLI subcommand. daemon_slug owner-unique validation.
3. **REQ-005** — service-token issuance daemon binding: `POST /auth/service-tokens`와 `pfhub service-token issue`가 existing daemon registry entry에 1:1 binding. unknown daemon 입력 명시적 거부.
4. **REQ-012** — service-token rotate daemon_id 불변식: rotate 시 `token_hash`, `token_prefix`, `runtime_id`만 갱신, `daemon_id` 유지. CLI rotate 출력에 `daemon_id` 포함.
5. **REQ-006** — GET /v1/assets daemon-scoped: `managed_by_daemon_id = token.daemon_id` 필터 적용. NULL token 403 fail-closed.
6. **REQ-007** — GET /v1/user_info daemon_id 필드: 응답 스키마에 `daemon_id` 추가. NULL backfill token은 `daemon_id: null` 진단 노출.
7. **REQ-008** — POST /v1/assets/{asset_id}/ledger/trades daemon assignment authorization: `managed_by_daemon_id = token.daemon_id` 검증. mismatch/미배정/NULL token 403 fail-closed.
8. **REQ-011** — baseline docs 갱신: `docs/interface-contract.md`, `docs/data-model.md`, `docs/runtime-flows.md`에 daemon binding/assignment/403 policy/rotate invariant 반영.
9. **REQ-028** — README.md 갱신: daemon provisioning 절차, enforcement enablement gate 4종 evidence, runtime_id/daemon_id 역할 구분 반영.
## Dependencies
- Internal:
- REQ-009 migration은 REQ-023~028 구현 전에 schema가 존재해야 한다.
- REQ-023 daemon registry가 구축된 후 REQ-005/012 binding 구현이 가능하다.
- REQ-005/012가 daemon binding을 제공한 후 REQ-006/007/008 daemon-scoped authorization이 가능하다.
- REQ-011/028 문서화는 구현 단계 마지막 또는 구현 완료 직후 수행한다.
- External:
- 기존 Alembic revision chain (최신 head revision과 연결 필요)
- 기존 service-token bearer auth 모델 유지 (read/trade-write/admin permission 불변)
## Design Gate
- Design Required Before Implementation: yes
- Reason: 신규 `Daemon` 엔티티의 API transport 형태(HTTP endpoint vs CLI subcommand vs 혼합), daemon_slug owner-unique constraint 설계, enforcement enablement gate 활성화 방식, NULL backfill 기간 동안의 fail-closed boundary 정의 등 구현 착수 전 확정이 필요한 설계 결정이 있다.
## Milestones
1. Design confirmed — daemon registry interface 형태, enforcement gate 활성화 방식 확정
2. REQ-009 migration 적용 + pytest 통과
3. REQ-023 daemon registry CRUD 구현 + targeted test 통과
4. REQ-005/012 daemon binding/rotate 구현 + targeted test 통과
5. REQ-006/007/008 daemon-scoped API 구현 + daemon A/B 분리 시나리오 테스트 통과
6. REQ-011/028 문서 갱신 완료 + 전체 pytest 통과
7. Verification — 전체 AC evidence 매핑 완료, ready for release
## Risks
- R-1 (High): NULL backfill 상태에서 daemon-scoped enforcement를 켜면 기존 service-token/asset이 연쇄 403에 직면해 운영 중단 발생 가능. 완화: enforcement는 inventory 완료 + dry-run zero-active-NULL/zero-mismatch + release approver 승인 이후에만 활성화.
- R-2 (Medium): daemon registry interface 형태 결정이 늦어지면 REQ-005/023 구현이 지연됨. 완화: design 단계에서 먼저 확정.
- R-3 (Medium): 기존 Alembic chain과 migration 충돌. 완화: 신규 revision을 기존 head에 선형으로 연결, `alembic history` 정상 여부 migration test에서 확인.
@@ -0,0 +1,132 @@
# 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 적용 검증은 배포 시 수행한다.
+2 -1
View File
@@ -16,4 +16,5 @@
| BAT-006 | standard | release-candidate | REQ-016, REQ-017, REQ-018, REQ-019 | dcy-004 | [bat-006_20260511_portfolio-uuid-migration](./bat-006_20260511_portfolio-uuid-migration/) |
| BAT-007 | batch-lite | release-candidate | REQ-001, REQ-002, REQ-003 | dcy-001 | [bat-007_20260511_local-db-validation-and-operations-runbook](./bat-007_20260511_local-db-validation-and-operations-runbook/) |
| BAT-008 | batch-lite | release-candidate | REQ-011 | dcy-002 | [bat-008_20260511_trex-basic-baseline-docs-update](./bat-008_20260511_trex-basic-baseline-docs-update/) |
| BAT-009 | batch-lite | release-candidate | REQ-020, REQ-021, REQ-022 | dcy-005 | [bat-009_20260512_production-operator-runbook-and-credential-matrix](./bat-009_20260512_production-operator-runbook-and-credential-matrix/) |
| BAT-009 | batch-lite | release-candidate | REQ-020, REQ-021, REQ-022 | dcy-005 | [bat-009_20260512_production-operator-runbook-and-credential-matrix](./bat-009_20260512_production-operator-runbook-and-credential-matrix/) |
| BAT-010 | standard | release-candidate | REQ-005, REQ-006, REQ-007, REQ-008, REQ-009, REQ-011, REQ-012, REQ-023, REQ-028 | dcy-006 | [bat-010_20260513_daemon-model-implementation](./bat-010_20260513_daemon-model-implementation/) |