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
+35 -1
View File
@@ -5,7 +5,7 @@ from __future__ import annotations
from datetime import datetime
from typing import Protocol
from app.domain.models import AuthToken, LedgerEntry, Portfolio, PortfolioComment, PortfolioTag, StrategySet, TokenPrincipal, User
from app.domain.models import AuthToken, Daemon, LedgerEntry, Portfolio, PortfolioComment, PortfolioTag, StrategySet, TokenPrincipal, User
class PortfolioRepository(Protocol):
@@ -53,6 +53,7 @@ class PortfolioRepository(Protocol):
token_type: str,
expires_at: datetime | None = None,
runtime_id: str | None = None,
daemon_id: str | None = None,
) -> AuthToken: ...
def list_auth_tokens(self, user_id: str) -> list[AuthToken]: ...
@@ -70,6 +71,30 @@ class PortfolioRepository(Protocol):
def get_token_principal(self, token_hash: str) -> TokenPrincipal | None: ...
def rotate_service_token(self, name: str) -> tuple[str, AuthToken]: ...
def create_daemon(
self,
owner_user_id: str,
*,
daemon_slug: str,
display_name: str | None = None,
) -> Daemon: ...
def get_daemon_by_id(self, daemon_id: str) -> Daemon | None: ...
def get_daemon_by_slug(self, owner_user_id: str, daemon_slug: str) -> Daemon | None: ...
def list_daemons_for_user(self, owner_user_id: str) -> list[Daemon]: ...
def update_daemon(
self,
daemon_id: str,
*,
display_name: str | None = None,
is_active: bool | None = None,
) -> Daemon: ...
def get(self, portfolio_id: str) -> Portfolio: ...
def get_or_create(
@@ -114,6 +139,15 @@ class PortfolioRepository(Protocol):
include_tags: list[str] | None = None,
exclude_tags: list[str] | None = None,
match_mode: str | None = None,
managed_by_daemon_id: str | None = None,
) -> list[Portfolio]: ...
def list_portfolios_with_strategies(
self,
owner_user_id: str,
*,
include_disabled: bool = True,
managed_by_daemon_id: str | None = None,
) -> list[Portfolio]: ...
def add_tag(self, portfolio_id: str, *, tag: str) -> PortfolioTag: ...