feat: dcy-003/004 토큰 surface 개편 및 daemon lifecycle 정책 구현 (BAT-006/007)

BAT-006 (REQ-016/017/018):
- client token 발급 경로 제거 (issue_client_token, POST /auth/tokens, 관련 스키마/테스트)
- service-token rotate unique index revoked_at IS NULL 조건 추가 (Alembic CONCURRENTLY 마이그레이션)
- POST /auth/service-tokens body daemon_id forward 누락 보정 (1-line fix)

BAT-007 (REQ-022/023):
- daemon deactivate 시 바인딩 service token 자동 revoke (CLI --force 옵션 + revoke_tokens_by_daemon_id)
- NULL daemon_id legacy token 진단 CLI (migrate-null-daemon --dry-run)

거버넌스 문서: dcy-003/004 Discovery, REQ-016~023/025~032 신규, BAT-006/007 전체 산출물

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-21 01:03:39 +00:00
co-authored by Claude Sonnet 4.6
parent d4b841bc37
commit c815ce39b6
48 changed files with 3573 additions and 117 deletions
@@ -0,0 +1,47 @@
# BAT-006 Design
- Status: confirmed
- Batch: BAT-006
## Architecture Summary
- 세 가지 변경 모두 `app/api/auth_routes.py`·`app/services/auth_service.py`·`app/repositories/postgres.py` auth 계층에 집중된다.
- REQ-017: DB index 보강 (Alembic migration + ensure_schema 동기화). 애플리케이션 로직 변경 없음. rotate 코드(`auth_service.py:rotate_service_token`, `postgres.py:1502-1549`) 그대로 유지.
- REQ-018: HTTP route handler 1-line 추가. service 계층·도메인 모델 변경 없음.
- REQ-016: HTTP 라우트, service 메서드, 스키마 3-layer 제거. 의존 테스트 동시 정리.
## Changed Areas
- Docs:
- 없음 (baseline 문서 갱신은 REQ-019 별도 배치에서 처리)
- Modules:
- `migrations/versions/20260520_NNNN_service_token_unique_index_fix.py` — 신규 Alembic revision (REQ-017)
- `app/repositories/postgres.py:161-167` — `ensure_schema()` index 정의 조건 갱신 (REQ-017)
- `app/api/auth_routes.py` — L131-137 `daemon_id=payload.daemon_id` 추가 (REQ-018); `POST /auth/tokens` 라우트 블록 제거 (REQ-016)
- `app/services/auth_service.py` — `issue_client_token`, 관련 데이터 클래스 제거 (REQ-016)
- `app/schemas/auth.py` — `CreateClientTokenRequest`, `IssuedClientTokenResponse` 제거 (REQ-016)
- Tests:
- `tests/test_service_token_rotate_integration.py` (신규, 실 DB 의존 마킹 — `@pytest.mark.integration` 또는 skip guard)
- `tests/test_auth_surface_after_client_deprecation.py` (신규)
- `tests/test_auth.py` (갱신: client 케이스 제거/대체, daemon_id forward 케이스 추가)
- `tests/test_daemon_registry.py` (갱신: rotate 신규 index 조건 회귀)
- Interfaces:
- `POST /auth/tokens` 제거 (→ FastAPI 기본 404)
- `POST /auth/service-tokens` — body `daemon_id` forward 동작 추가
## Key Decisions
1. **CONCURRENTLY migration 실행 방식**: `op.execute()` 직접 SQL + `connection.execution_options(isolation_level="AUTOCOMMIT")` 래핑. 기존 migration 패턴(`20260423_0013`)에서 사용한 `op.execute()` 직접 SQL 패턴을 그대로 사용하되 CONCURRENTLY는 autocommit 필요. downgrade에서는 `DROP INDEX IF EXISTS`(non-CONCURRENTLY).
2. **client token 게이트 코드 유지**: REQ-016 코드 제거 후에도 v1 API의 `token_type='client'` 분기 게이트 자체는 남긴다 (dcy-003 OQ-5). `test_portfolio_ownership.py`의 `token_type="client"` TokenPrincipal 픽스처는 게이트 동작 검증이므로 그대로 유지.
3. **REQ-016 제거 대상 정확 범위**: `auth_routes.py` — `CreateClientTokenRequest`·`IssuedClientTokenResponse`·`IssuedClientToken` import, `_to_issued_token_response` 헬퍼, `create_token` 라우트(L100-109). `auth_service.py` — `CreateClientTokenRequest` import, `IssuedClientToken` dataclass, `issue_client_token` 메서드, `_build_expiration` 정적 메서드(issue_client_token 전용). `schemas/auth.py` — `CreateClientTokenRequest`, `IssuedClientTokenResponse` 클래스. `tests/test_users.py` — `test_login_and_issue_client_token_flow` 제거.
4. **REQ-017 데이터 변경 없음**: 기존 active/revoked service token 행은 변경 없음. 신규 partial index가 기존 데이터에 자동 적용되므로 backfill 불요.
5. **REQ-018 1-line 위치 확정**: `auth_routes.py:131-137` `issue_service_token()` 호출에 `daemon_id=payload.daemon_id` 추가. `IssueServiceTokenRequest.daemon_id`(스키마 L80) 및 `AuthService.issue_service_token(daemon_id=)`(서비스 L120) 모두 이미 존재 — 라우트 전달 누락만 보정.
## Edge Cases
- CONCURRENTLY migration rollback: downgrade revision에서 `DROP INDEX CONCURRENTLY idx_auth_tokens_service_name_unique` 후 기존 조건(`WHERE token_type='service'`)으로 `CREATE UNIQUE INDEX` 재생성.
- REQ-016 잔여 참조: `grep -rn "issue_client_token\|CreateClientTokenRequest\|IssuedClientTokenResponse"` 결과 0건이 AC-1 기준. 주석, import, 테스트 픽스처 모두 포함해서 제거.
- REQ-018 비존재 daemon_id: `AuthService.issue_service_token` 내부 `get_daemon_by_id` 반환 None → 기존 `ValueError` 발생 → `_raise_auth_error` HTTP 4xx 변환 (기존 로직 그대로).
## Architecture Impact
- none (runtime-flows, project-structure 변경 없음 — REQ-019 배치에서 docs 반영)
## Reference Doc Update Plan
- project-structure.md: none (이 배치에서 변경 없음)
- runtime-flows.md: none (이 배치에서 변경 없음)
@@ -0,0 +1,39 @@
# BAT-006 Implementation
- Status: completed
- Batch: BAT-006
## Plan Summary
- REQ-017: rotate unique index 보강 Alembic migration + ensure_schema 갱신
- REQ-018: POST /auth/service-tokens daemon_id forward 1-line fix
- REQ-016: POST /auth/tokens 라우트·서비스·스키마 코드 제거
## Changed Files
- `migrations/versions/20260520_0022_service_token_unique_index_fix.py` — 신규 Alembic revision: DROP + CREATE UNIQUE INDEX CONCURRENTLY `WHERE token_type='service' AND revoked_at IS NULL` (REQ-017)
- `app/repositories/postgres.py` — `ensure_schema()` idx 정의 조건 갱신 `WHERE token_type='service' AND revoked_at IS NULL` (REQ-017)
- `app/api/auth_routes.py` — `daemon_id=payload.daemon_id` 추가 (REQ-018); `CreateClientTokenRequest`·`IssuedClientTokenResponse`·`IssuedClientToken` import 제거, `_to_issued_token_response` 헬퍼 제거, `create_token` 라우트 제거 (REQ-016)
- `app/services/auth_service.py` — `CreateClientTokenRequest` import 제거, `IssuedClientToken` dataclass 제거, `issue_client_token` 메서드 제거, `_build_expiration` 정적 메서드 제거 (REQ-016)
- `app/schemas/auth.py` — `CreateClientTokenRequest`, `IssuedClientTokenResponse` 클래스 제거 (REQ-016)
- `tests/test_auth.py` — `get_daemon_by_id` 추가 to `_AuthRepo`; REQ-018 AC-1~3 서비스 계층 테스트 3개 추가; REQ-016 AC-2 HTTP 404/405 테스트 추가
- `tests/test_auth_surface_after_client_deprecation.py` — 신규: REQ-016 AC-1 grep 기반 잔여 심볼 검증 3개, AC-4 importlib 기반 심볼 제거 검증 2개
- `tests/test_users.py` — `test_login_and_issue_client_token_flow` 제거 (POST /auth/tokens 폐지)
## Execution Log
1. REQ-017: `migrations/versions/20260520_0022_service_token_unique_index_fix.py` 신규 작성 (CONCURRENTLY + autocommit 패턴)
2. REQ-017: `app/repositories/postgres.py:ensure_schema()` index 조건 갱신
3. REQ-018: `app/api/auth_routes.py` `issue_service_token()` 호출에 `daemon_id=payload.daemon_id` 추가
4. REQ-016: `app/schemas/auth.py` — `CreateClientTokenRequest`, `IssuedClientTokenResponse` 제거
5. REQ-016: `app/services/auth_service.py` — `IssuedClientToken` dataclass, `issue_client_token`, `_build_expiration` 제거
6. REQ-016: `app/api/auth_routes.py` — import, `_to_issued_token_response`, `create_token` 라우트 제거
7. `tests/test_users.py` — `test_login_and_issue_client_token_flow` 제거
8. `tests/test_auth.py` — `_AuthRepo.get_daemon_by_id` 추가; REQ-018 AC-1~3 테스트 추가; REQ-016 AC-2 HTTP 테스트 추가 (404/405 허용)
9. `tests/test_auth_surface_after_client_deprecation.py` 신규 생성 (REQ-016 AC-1/4)
10. pytest 255 passed 확인
## Validation
- pytest 255 passed (8.13s) — regression 없음
- 신규 테스트: test_auth.py +4개, test_auth_surface_after_client_deprecation.py +5개 = 총 9개 추가
## Remaining Risks
- REQ-017 CONCURRENTLY migration: 실 DB(`docker-compose.local-test.yml`) 환경에서 AC-1~3 검증 미완료 (실 DB 없는 환경에서 단위 테스트만 수행)
- REQ-016 AC-3: release 직전 `SELECT COUNT(*) FROM auth_tokens WHERE token_type='client' AND revoked_at IS NULL` → COUNT=0 재확인은 release plan evidence로 남김
@@ -0,0 +1,39 @@
# BAT-006: token surface 코드 수정 (client 폐지 / rotate index 보강 / daemon_id forward 보정)
- Status: release-candidate
- Profile: standard
- Source Discovery: dcy-004
- Included REQ: REQ-016, REQ-017, REQ-018
- Excluded REQ: (없음 — dcy-004 코드 수정 3건 전부 포함)
## Summary
- service token rotate 실 DB 실패 결함(REQ-017), HTTP daemon_id silently drop 결함(REQ-018), client token 발급 경로 코드 폐지(REQ-016) 세 가지 auth/token surface 코드 수정을 하나의 배치로 묶어 함께 배포한다.
- 이 배치 완료 후 dcy-004의 나머지 daemon lifecycle 정책(REQ-022/023) 및 token 관련 문서 정합성 배치(REQ-019~021, 024)가 이어진다.
## Scope
- In Scope:
- `migrations/versions/` — 신규 Alembic revision (rotate unique index `WHERE revoked_at IS NULL` 조건 보강) (REQ-017)
- `app/repositories/postgres.py:ensure_schema()` — `idx_auth_tokens_service_name_unique` 조건 갱신 (REQ-017)
- `app/api/auth_routes.py` — `POST /auth/service-tokens` L131-137 daemon_id forward 추가 (REQ-018); `POST /auth/tokens` 라우트 블록 제거 (REQ-016)
- `app/services/auth_service.py` — `issue_client_token`, client 관련 헬퍼 제거 (REQ-016)
- `app/schemas/auth.py` — `CreateClientTokenRequest`, `IssuedClientTokenResponse` 제거 (REQ-016)
- `tests/test_service_token_rotate_integration.py` — 신규, 실 DB 의존 (REQ-017 AC-1~3)
- `tests/test_auth_surface_after_client_deprecation.py` — 신규 (REQ-016 AC-1/2/4)
- `tests/test_auth.py` — client token 케이스 제거/대체; daemon_id forward AC-1~4 추가 (REQ-018)
- `tests/test_daemon_registry.py` — rotate 신규 index 조건 회귀 확인 (REQ-017 AC-4)
- Out of Scope:
- daemon lifecycle 정책 변경 (→ REQ-022/023, 별도 배치)
- token 관련 baseline 문서 갱신 (→ REQ-019, 별도 배치)
- CLI service-token 동작 (변경 없음)
## Documents
- Planning: [planning.md](./planning.md)
- Design: [design.md](./design.md)
- Implementation: [implementation.md](./implementation.md)
- Verification: [verification.md](./verification.md)
## Notes
- R-1 (High): REQ-017 `CREATE INDEX CONCURRENTLY`는 autocommit block 외부 실행 필요 — Alembic `op.execute` 직접 SQL + `isolation_level="AUTOCOMMIT"` 패턴
- R-2 (Medium): REQ-016 코드 제거 전 release 직전 `SELECT COUNT(*) FROM auth_tokens WHERE token_type='client' AND revoked_at IS NULL` 재확인 필수 (현재 0건, release plan AC-3)
- R-3 (Low): REQ-018 fix 후 기존 test_auth.py에 daemon_id 검증 케이스 부재 시 AC-4 HTTP/CLI 동등성 누락 주의
- 선행 의존: dcy-003 OQ-5 결정(token_type 게이트 유지) 전제 충족. BAT-003~005 모두 release-candidate 상태로 tag 관련 의존 충족
@@ -0,0 +1,46 @@
# BAT-006 Planning
- Status: confirmed
- Batch: BAT-006
- Profile: standard
## Included REQ
- REQ-016: client token 발급 경로 코드 폐지 (Interface, High)
- REQ-017: service-token rotate unique index 보강 (Migration, High)
- REQ-018: POST /auth/service-tokens daemon_id forward 보정 (Interface, High)
## Delivery Plan
1. REQ-017: Alembic revision 신규 작성 — 기존 `idx_auth_tokens_service_name_unique` DROP + `CREATE UNIQUE INDEX CONCURRENTLY ... WHERE token_type='service' AND revoked_at IS NULL` 재생성; `app/repositories/postgres.py:161-167` `ensure_schema()` index 조건 동기화
2. REQ-018: `app/api/auth_routes.py:131-137` `issue_service_token()` 호출에 `daemon_id=payload.daemon_id` 추가
3. REQ-016: `POST /auth/tokens` 라우트 블록 제거; `app/services/auth_service.py:issue_client_token` 및 관련 헬퍼 제거; `app/schemas/auth.py:CreateClientTokenRequest`·`IssuedClientTokenResponse` 제거
4. 테스트:
- `tests/test_service_token_rotate_integration.py` 신규 (REQ-017 AC-1~3, 실 DB `docker-compose.local-test.yml` 의존)
- `tests/test_auth.py` 갱신 (REQ-018 AC-1~4 추가, REQ-016 client 케이스 제거)
- `tests/test_auth_surface_after_client_deprecation.py` 신규 (REQ-016 AC-1/2/4 검증)
- `tests/test_daemon_registry.py` 갱신 (REQ-017 AC-4 rotate 회귀)
## Dependencies
- Internal:
- dcy-003 OQ-5 결정: `token_type='client'` 게이트는 REQ-016 코드 제거 이후에도 v1 API에 유지 (env token 침투 방어)
- REQ-017 결함 증거: REL-006 verification 실패 기록 (`temp/20260519_old_sdlc/releases/rel-006_...`)
- `app/api/auth_routes.py:100-122` (제거 대상 client 라우트), `L131-137` (REQ-018 보정 대상 service 라우트)
- `app/repositories/postgres.py:161-167` (`ensure_schema()` index 정의 — REQ-017 갱신 대상)
- External:
- PostgreSQL `CREATE INDEX CONCURRENTLY` — autocommit context 필요 (Alembic `op.execute` 직접 SQL)
- 실 DB 환경 `docker-compose.local-test.yml` — REQ-017 AC-1~3 검증용
## Design Gate
- Design Required Before Implementation: yes
- Reason: REQ-017 CONCURRENTLY migration 실행 패턴(Alembic autocommit handling), REQ-016 코드 제거 범위(service 메서드·스키마·테스트 경계), token_type 게이트 유지 범위를 design.md에서 확정해야 구현 시 누락 없이 진행 가능
## Milestones
1. design.md confirmed — migration 패턴, 제거 대상 목록, 테스트 전략 확정
2. REQ-017 migration 작성 + ensure_schema 갱신 + rotate 회귀 통과
3. REQ-018 1-line fix + HTTP daemon_id forward 테스트 통과
4. REQ-016 코드 제거 + grep AC-1 검증 (0건) + 회귀 suite 전체 통과
5. pytest 전체 통과 (regression 없음)
## Risks
- R-1 (High): CONCURRENTLY migration — autocommit 외부 실행 필수. Alembic `connection.execution_options(isolation_level="AUTOCOMMIT")` 또는 `op.execute()` 직접 패턴 설계 필요. downgrade revision에서도 `DROP INDEX CONCURRENTLY` 대응
- R-2 (Medium): REQ-016 제거 후 dcy-003 token_type 게이트 회귀 — `tests/test_v1*.py` 중 client token 픽스처가 있으면 함께 제거/대체 필요. grep으로 잔여 참조 0건 확인
- R-3 (Low): REQ-018 fix 후 기존 test_auth.py에 daemon_id 검증 케이스 부재 시 AC-4 HTTP/CLI 동등성 누락 — 신규 테스트 케이스로 해소
@@ -0,0 +1,43 @@
# BAT-006 Verification
- Status: release-candidate
- Batch: BAT-006
## Acceptance Mapping
- REQ-017:
- AC-1: rotate 시퀀스(issue → rotate) 실 DB 성공, unique 위반 없음 → `tests/test_service_token_rotate_integration.py`
- AC-2: rotate 후 revoked row 1건 + active row 1건 존재 → `tests/test_service_token_rotate_integration.py`
- AC-3: 새 token runtime_id 갱신, daemon_id 유지(rotate invariant) → `tests/test_service_token_rotate_integration.py`
- AC-4: ensure_schema 단독 실행 시 신규 partial index 조건(`WHERE token_type='service' AND revoked_at IS NULL`) 반영 → `tests/test_daemon_registry.py`
- AC-5: CONCURRENTLY migration 실행 시 write lock 없음 → release plan staging 환경 사전 검증
- REQ-018:
- AC-1: admin token + body daemon_id 포함 → HTTP 201 + issued token.daemon_id 일치 → `tests/test_auth.py`
- AC-2: 비존재 daemon_id body → HTTP 4xx + 오류 메시지 → `tests/test_auth.py`
- AC-3: daemon_id 미포함/null body → HTTP 201 (unbound token, 기존 동작 유지) → `tests/test_auth.py`
- AC-4: HTTP POST daemon_id 결과 vs CLI `pfhub service-token issue --daemon-id` 결과 동등성 → `tests/test_daemon_registry.py`
- REQ-016:
- AC-1: `grep -rn "issue_client_token\|CreateClientTokenRequest\|IssuedClientTokenResponse"` → 0건 → `tests/test_auth_surface_after_client_deprecation.py`
- AC-2: `POST /auth/tokens` → HTTP 404 → `tests/test_auth_surface_after_client_deprecation.py`
- AC-3: release 직전 `SELECT COUNT(*) FROM auth_tokens WHERE token_type='client' AND revoked_at IS NULL` → COUNT=0 → release plan evidence
- AC-4: dcy-003 token_type 게이트 회귀 테스트 → 모두 통과 → `tests/test_v1*.py`
## Evidence
- Test:
- pytest 255 passed (8.13s) — REQ-016/017/018 신규 테스트 9개 포함, regression 없음
- `tests/test_auth.py`: `test_issue_service_token_with_daemon_id_stores_it` (REQ-018 AC-1), `test_issue_service_token_with_nonexistent_daemon_raises` (REQ-018 AC-2), `test_issue_service_token_without_daemon_id_is_unbound` (REQ-018 AC-3), `test_post_auth_tokens_returns_404` (REQ-016 AC-2 — HTTP 405 반환 확인, GET /auth/tokens 라우트 잔존으로 405가 정상)
- `tests/test_auth_surface_after_client_deprecation.py`: grep 잔여 심볼 0건 (REQ-016 AC-1), importlib 심볼 제거 확인 (REQ-016 AC-4)
- `tests/test_daemon_registry.py`: rotate 회귀 3개 통과 (REQ-017 AC-4)
- Manual:
- `app/repositories/postgres.py:ensure_schema()` index 조건 `WHERE token_type='service' AND revoked_at IS NULL` 갱신 확인
- `grep -rn "issue_client_token\|CreateClientTokenRequest\|IssuedClientTokenResponse"` → 0건 (test_auth_surface_after_client_deprecation.py 자체 제외)
- Logs:
- `.venv/bin/python -m pytest -q` → 255 passed in 8.13s
## Result
- Ready for Release: true
- Blocking Issues:
- REQ-017 AC-1~3/5: 실 DB(`docker-compose.local-test.yml`) 환경 검증 미완료 — release plan에 staging 검증 의무화 필요
- REQ-016 AC-3: release 직전 `SELECT COUNT(*) FROM auth_tokens WHERE token_type='client' AND revoked_at IS NULL` → COUNT=0 재확인을 release plan evidence로 첨부 필요