Moves phases 08–11 execution artifacts from .planning/phases/ to .planning/milestones/v0.2-phases/ to keep .planning/phases/ clean for the next milestone. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
21 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, tags, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | tags | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 08-stack-upgrade-backend-decomposition | 04 | execute | 2 |
|
|
true |
|
|
|
Purpose: CODE-01 (decomposition) + CODE-08 (single definition of the provider_must_be_known rule, plus the already-completed CloudConnectionOut migration from plan 08-02).
Output: backend/api/admin/ package; one new helper in services/ai_config.py; backend/api/admin.py monolith file deleted.
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/08-stack-upgrade-backend-decomposition/08-CONTEXT.md @.planning/phases/08-stack-upgrade-backend-decomposition/08-RESEARCH.md @.planning/phases/08-stack-upgrade-backend-decomposition/08-PATTERNS.md @backend/api/admin.py @backend/api/folders.py @backend/api/cloud.py @backend/api/schemas.py @backend/services/ai_config.py @backend/ai/provider_config.py @backend/main.py @CLAUDE.md Endpoint to sub-module map (locked per RESEARCH.md §"Recommended sub-module assignment"):users.py: list_users (GET /users), create_user (POST /users), update_user_status (PATCH /users/{id}/status), initiate_password_reset (POST /users/{id}/password-reset), update_ai_config (PATCH /users/{id}/ai-config), delete_user (DELETE /users/{id}), create_system_topic (POST /topics) quotas.py: get_user_quota (GET /users/{id}/quota), update_user_quota (PATCH /users/{id}/quota) ai.py: get_ai_config_models (GET /ai-config/models), test_ai_connection (POST /ai-config/test-connection), get_ai_config (GET /ai-config), update_system_ai_config (PUT /ai-config)
Pydantic model to file map:
users.py: UserCreate, UserStatusUpdate, UserAiConfigUpdate, UserDeleteConfirm, SystemTopicCreate quotas.py: QuotaUpdate ai.py: SystemAiConfigUpdate, TestConnectionRequest
Helper map:
shared.py: _user_to_dict (used by users.py and quotas.py) ai.py: _ai_config_to_dict (used only by ai.py — keeps it local)
New helper to add (D-11 migration) in backend/services/ai_config.py:
def validate_provider_id(v: str) -> str: """Service-layer provider_id validator; raises ValueError per CLAUDE.md service-vs-API rule.""" if v not in PROVIDER_DEFAULTS: raise ValueError(f"Unknown provider_id {v!r}. Must be one of: {list(PROVIDER_DEFAULTS.keys())}") return v
Package aggregator pattern for backend/api/admin/__init__.py:
from fastapi import APIRouter from api.admin.users import router as users_router from api.admin.quotas import router as quotas_router from api.admin.ai import router as ai_router
router = APIRouter(prefix="/api/admin", tags=["admin"]) router.include_router(users_router) router.include_router(quotas_router) router.include_router(ai_router)
Sub-router pattern in each sub-module (D-04 — NO prefix on the sub-router):
router = APIRouter() # NO prefix — parent __init__.py carries it @router.get("/users") # becomes /api/admin/users via parent
Task 1: Add validate_provider_id helper to services/ai_config.py (D-11 migration) backend/services/ai_config.py - backend/services/ai_config.py (current contents — confirm PROVIDER_DEFAULTS is already imported at line 34) - backend/api/admin.py lines 147-179 (current inline validators in SystemAiConfigUpdate and TestConnectionRequest) - backend/ai/provider_config.py (confirm PROVIDER_DEFAULTS keys) Add a new top-level function `validate_provider_id(v: str) -> str` to `backend/services/ai_config.py`. The function MUST: (a) take a single string argument, (b) raise `ValueError(f"Unknown provider_id {v!r}. Must be one of: {list(PROVIDER_DEFAULTS.keys())}")` if `v not in PROVIDER_DEFAULTS`, (c) otherwise return `v` unchanged. Place the function after the existing `load_provider_config` helpers, before any HKDF helpers. PROVIDER_DEFAULTS is already imported at line 34 of the file. Add a single-line docstring: `"""Service-layer provider_id validator; raises ValueError per CLAUDE.md service-vs-API rule."""`. Do not modify any existing function in this file. cd backend && python -c "from services.ai_config import validate_provider_id; assert validate_provider_id('openai') == 'openai'; thrown=False try: validate_provider_id('not-a-provider') except ValueError as e: assert 'Unknown provider_id' in str(e); thrown=True assert thrown; print('ok')" - `grep -c "^def validate_provider_id" backend/services/ai_config.py` returns 1 - Running `python -c "from services.ai_config import validate_provider_id; validate_provider_id('openai')"` from `backend/` exits 0 - Running `python -c "from services.ai_config import validate_provider_id; validate_provider_id('xxx')"` from `backend/` exits non-zero with `ValueError` - No existing function in `backend/services/ai_config.py` was modified (verify by reading the file) validate_provider_id function exists, raises ValueError per CLAUDE.md service-layer rule, importable. Task 2: Create backend/api/admin/ package — shared.py, users.py, quotas.py, ai.py, __init__.py backend/api/admin/__init__.py, backend/api/admin/shared.py, backend/api/admin/users.py, backend/api/admin/quotas.py, backend/api/admin/ai.py - backend/api/admin.py lines 1-934 (full source — every endpoint, every Pydantic model, every helper) - backend/api/folders.py (analog sub-router structure, ownership check pattern, error handling style) - .planning/phases/08-stack-upgrade-backend-decomposition/08-PATTERNS.md sections for `backend/api/admin/*` (exact import lists, sub-router declaration, auth guard, ValueError-to-HTTPException bridge) - CLAUDE.md §"Backend: shared module map" (must not violate; helpers go in deps/utils.py / services/auth.py / api/admin/shared.py — never duplicated) This task creates the package `backend/api/admin/` and ALL five files in one logical unit. Execute in this order so the package is in a valid state on disk after each sub-step:(A) Create backend/api/admin/shared.py first. Add from __future__ import annotations. Import User, SystemSettings from db.models. Define _user_to_dict(user: User) -> dict exactly per the body at backend/api/admin.py lines 75-91 (copy verbatim — same field set, same created_at.isoformat() handling, same docstring). The _ai_config_to_dict helper does NOT go in shared.py — it goes in ai.py because only ai.py uses it.
(B) Create backend/api/admin/users.py. Header: from __future__ import annotations. Imports per PATTERNS.md §"backend/api/admin/users.py" plus from api.admin.shared import _user_to_dict. Declare router = APIRouter() with NO prefix (D-04). Move these Pydantic models verbatim from backend/api/admin.py: UserCreate (lines 96-107), UserStatusUpdate (109-111), UserAiConfigUpdate (124-127), UserDeleteConfirm (190-195), SystemTopicCreate (182-187). Move these handler functions verbatim including decorators from backend/api/admin.py: list_users (228-243), create_user (245-316), update_user_status (318-383), initiate_password_reset (385-415), update_ai_config (494-534 — the per-user one), delete_user (536-637 — confirm exact end line by reading source), create_system_topic (639-662). Every handler keeps its _admin: User = Depends(get_current_admin) injection — never omit.
(C) Create backend/api/admin/quotas.py. Header + imports per PATTERNS.md §"backend/api/admin/quotas.py", plus from api.admin.shared import _user_to_dict. router = APIRouter() no prefix. Move QuotaUpdate Pydantic model (lines 113-121). Move handlers get_user_quota (417-439) and update_user_quota (441-492). Both inject _admin: User = Depends(get_current_admin).
(D) Create backend/api/admin/ai.py. Header + imports per PATTERNS.md §"backend/api/admin/ai.py". Add from services.ai_config import encrypt_api_key, load_provider_config_by_id, validate_provider_id (note the new validate_provider_id from task 1). router = APIRouter() no prefix. Move _ai_config_to_dict helper (lines 58-73) here as a module-level private helper (not in shared.py — only ai.py uses it). Move Pydantic models SystemAiConfigUpdate (129-155) and TestConnectionRequest (157-179). In both models, replace the inline provider_must_be_known validator body with return validate_provider_id(v) — the validator becomes a one-line call-through to the service-layer function (D-11). Move handlers get_ai_config_models (664-725), test_ai_connection (727-784), get_ai_config (786-824), update_system_ai_config (826-934). Every handler injects _admin: User = Depends(get_current_admin).
(E) Create backend/api/admin/__init__.py. Contents per PATTERNS.md §"backend/api/admin/__init__.py": import APIRouter from fastapi, import router as users_router, router as quotas_router, router as ai_router from the three sub-modules, declare router = APIRouter(prefix="/api/admin", tags=["admin"]), call router.include_router(users_router), router.include_router(quotas_router), router.include_router(ai_router). The __init__.py does ONLY router aggregation — no helpers, no models, no logic (Pitfall 2 prevention).
Do NOT delete backend/api/admin.py yet — that happens in task 3 after URL regression passes. Do NOT modify backend/main.py — the existing from api.admin import router as admin_router will continue to work after task 3 because the package's __init__.py exports router.
IMPORTANT URL regression: After creating the package, Python's import resolution prefers the package (directory with __init__.py) over the module (admin.py file) ONLY if both exist at the same level — but here they collide. To avoid an import ambiguity error, ensure the package directory is created and immediately rename backend/api/admin.py to backend/api/admin_OLD_REMOVE_IN_TASK_3.py as part of the same atomic edit. This sidelines the monolith without deleting it so task 3 can confirm tests pass and then delete the renamed file.
cd backend && python -c "from api.admin import router; print('admin routes:', len(router.routes))" && python -c "from api.admin.users import router as r; print('users routes:', len(r.routes))" && python -c "from api.admin.quotas import router as r; print('quotas routes:', len(r.routes))" && python -c "from api.admin.ai import router as r; print('ai routes:', len(r.routes))"
<acceptance_criteria>
- Files exist: backend/api/admin/__init__.py, backend/api/admin/shared.py, backend/api/admin/users.py, backend/api/admin/quotas.py, backend/api/admin/ai.py
- grep -v '^#' backend/api/admin/__init__.py | grep -c 'router = APIRouter(prefix="/api/admin"' returns 1
- grep -v '^#' backend/api/admin/users.py | grep -c 'router = APIRouter()' returns 1 (NO prefix)
- grep -v '^#' backend/api/admin/quotas.py | grep -c 'router = APIRouter()' returns 1 (NO prefix)
- grep -v '^#' backend/api/admin/ai.py | grep -c 'router = APIRouter()' returns 1 (NO prefix)
- grep -c "Depends(get_current_admin)" backend/api/admin/users.py ≥ 7 (one per handler)
- grep -c "Depends(get_current_admin)" backend/api/admin/quotas.py ≥ 2
- grep -c "Depends(get_current_admin)" backend/api/admin/ai.py ≥ 4
- grep -c "return validate_provider_id(v)" backend/api/admin/ai.py returns 2 (SystemAiConfigUpdate + TestConnectionRequest)
- cd backend && python -c "from api.admin import router; assert len(router.routes) >= 13" exits 0 (total: 7 users + 2 quotas + 4 ai = 13)
- cd backend && python -c "from main import app; routes = [r.path for r in app.routes]; assert '/api/admin/users' in routes" 2>&1 | tail -3 shows no AssertionError
</acceptance_criteria>
Package exists with all five files; sub-router count totals 13; no sub-router carries a prefix; main.py imports still resolve; admin.py monolith is renamed but not yet deleted.
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| Unauthenticated/regular user → admin endpoints | All sub-router handlers must enforce get_current_admin to prevent privilege escalation |
| Admin endpoint → CloudConnectionOut serialization | SEC-08: credentials_enc must remain absent from the schema after the move |
| Admin endpoint → response body | T-02-27 / SEC-07: _user_to_dict must continue to exclude password_hash, credentials_enc, totp_secret, document content |
STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|---|---|---|---|---|
| T-08-04-01 | Spoofing/Elevation of Privilege | Admin sub-router auth gate | mitigate | Every handler in users.py / quotas.py / ai.py declares _admin: User = Depends(get_current_admin). Acceptance criterion counts these injections. |
| T-08-04-02 | Tampering | Sub-router prefix doubling | mitigate | Sub-routers declare router = APIRouter() with NO prefix per D-04; only __init__.py carries prefix="/api/admin". Acceptance criterion greps for this. |
| T-08-04-03 | Information Disclosure | _user_to_dict field set |
mitigate | Helper copied verbatim from admin.py lines 75-91; existing tests covering admin user list endpoint guard against accidental field inclusion. |
| T-08-04-04 | Denial of Service | Circular import via __init__.py |
mitigate | __init__.py does ONLY aggregation; _user_to_dict lives in shared.py not __init__.py (Pitfall 2). |
| T-08-04-05 | Tampering | Validator duplication | mitigate | provider_must_be_known migrated to services/ai_config.py as validate_provider_id; both Pydantic models call the same service helper (D-11). |
| T-08-04-06 | Information Disclosure | Old admin.py left in place | mitigate | Task 3 deletes the file only after URL regression tests pass; renamed file as an intermediate state cannot be imported because no code imports from api.admin_OLD_REMOVE_IN_TASK_3. |
| T-08-04-SC | Supply Chain | No new packages | accept | This plan installs zero new packages. |
| </threat_model> |
<success_criteria>
backend/api/admin/package with 5 filesbackend/api/admin.pydeleted- All admin URL paths unchanged
- CODE-08: CloudConnectionOut + provider_must_be_known single-defined
- All tests pass </success_criteria>