# 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).