# BAT-019 Design - Status: confirmed - Batch: BAT-019 ## Architecture Summary 이 batch는 두 가지 독립적인 변경으로 구성된다. 1. **REQ-044**: `GET /v1/assets`의 인증 게이트를 `require_admin` → `require_read`로 완화. 기존 `AssetCatalogListResponse` 응답 구조, 필터 파라미터, 라우터 등록은 모두 불변. 2. **REQ-045**: `GET /v1/daemons` 신규 엔드포인트. caller(`auth.user_id`) 소유 데몬만 반환. `require_read` 인증. 신규 파일: `app/api/v1_daemons.py`, 신규 스키마: `DaemonResponse`, `DaemonListResponse`. `app/main.py`에 라우터 등록. ## Changed Areas - Docs: `docs/batches/bat-019_20260627_assets-read-permission-and-daemon-list/` - Modules: - `app/api/v1_assets.py` (require_admin → require_read, list_catalog_assets만) - `app/api/v1_daemons.py` (신규) - `app/schemas/v1.py` (DaemonResponse, DaemonListResponse 추가) - `app/main.py` (v1_daemons 라우터 include_router 추가) - Interfaces: - `GET /v1/assets`: 기존 인터페이스 불변, 인증 게이트만 완화 - `GET /v1/daemons`: 신규 엔드포인트 ## Key Decisions 1. **REQ-044 — 권한 완화 범위**: `list_catalog_assets`(GET /v1/assets) 1개만 `require_read`로 교체. POST /v1/assets, PATCH /{id}, PATCH /{id}/deactivate, POST /{id}/tags, DELETE /{id}/tags/{tid}는 `require_admin` 유지. 응답 스키마 `AssetCatalogListResponse`는 불변. 2. **REQ-045 — 경로**: `GET /v1/daemons`. `/v1/user/daemons` 대신 `/v1/daemons` 채택 — 기존 `/v1/tags`, `/v1/assets`, `/v1/portfolios` 패턴과 일치. 3. **REQ-045 — 응답 스키마**: `DaemonListResponse{daemons: list[DaemonResponse], total: int}`. `DaemonResponse` 필드: `id`(str), `daemon_slug`(str), `display_name`(str|None), `is_active`(bool). 기존 `TagListResponse{tags: list[TagItem], total: int}` 패턴과 동일 구조. 4. **REQ-045 — env/service 토큰(user_id=None) 처리**: `auth.user_id`가 None이면 빈 목록 `[]` + `total: 0`을 HTTP 200으로 반환. 이유: 환경 변수 토큰 또는 user_id 없는 서비스 토큰은 특정 사용자 소유 데몬이 없으므로 에러보다 빈 응답이 자연스럽다. 기존 `/v1/user_info`의 env 토큰 처리(403 Service token required)와 다른 접근이며, 데몬 목록은 사용자 식별 없이도 빈 목록을 반환하는 것이 호출자 친화적이다. 5. **REQ-045 — active 필터**: 기본적으로 소유 데몬 전체 반환(active/inactive 모두). `is_active` 필드로 구분 표시. 이유: caller가 비활성 데몬도 인지해야 할 수 있으며(재활성화 등), 필터는 UI 레이어에서 처리 가능. 6. **REQ-045 — AuthService 활용**: `AuthService.find_candidate_daemons_for_user(user_id)` 호출. 단, 라우터에서 직접 repository를 호출하는 기존 패턴(v1_assets, v1_tags 등)과 일관성을 위해 라우터에서 직접 `repo.list_daemons_for_user(user_id)`를 호출하는 방식 채택 (service 레이어 DI overhead 없이 기존 패턴 유지). 7. **REQ-044 테스트 영향**: 기존 테스트 중 `test_get_v1_assets_user_token_returns_403`, `test_get_v1_assets_service_token_returns_403`는 이번 완화로 동작이 바뀜. — user 토큰(permission="read"): 이제 200이어야 함 → 테스트 업데이트. — service 토큰(permission="trade-write"): trade-write는 read 포함(_has_permission 로직상)이므로 200이어야 함 → 테스트 업데이트. 또한 `test_v1_assets_rejects_env_token`(test_v1.py): env read 토큰도 이제 200이어야 함 → 업데이트. `test_v1_assets_returns_asset_list`: read 토큰이 이제 403이 아닌 200 → 업데이트. `test_v1_assets_market_filter`, `test_v1_assets_asset_id_is_uuid`: 동일. ## Edge Cases - `user_id=None`(env 토큰): 빈 목록 200 반환 (결정 #4). - 데몬 없는 사용자: 빈 목록 `[]` 200 반환. - admin 토큰도 caller 스코프: admin 토큰의 `user_id`가 None이면 빈 목록, user_id가 있으면 해당 user 소유 데몬만. - `list_daemons_for_user` DB 미활성(db_enabled=False): 빈 목록 반환 또는 InMemory 구현에 따름. ## Architecture Impact - none (라우터 추가는 runtime-flows 수준 변경이 아님; project-structure에 신규 파일 기록 수준) ## Reference Doc Update Plan - project-structure.md: none (파일 추가 수준, 별도 문서화 불필요) - runtime-flows.md: none