--- phase: 08-stack-upgrade-backend-decomposition plan: 02 type: execute wave: 0 depends_on: [] files_modified: - backend/api/schemas.py - backend/api/cloud.py autonomous: true requirements: [CODE-08] tags: [shared-schemas, cross-package, prerequisite, wave-0] must_haves: truths: - "backend/api/schemas.py exists and defines CloudConnectionOut with field_validator coerce_id_to_str" - "backend/api/cloud.py imports CloudConnectionOut from api.schemas (not api.admin)" - "All cloud endpoints continue to return the same JSON shape as before" - "CloudConnectionOut definition appears exactly once across the entire backend tree" artifacts: - path: "backend/api/schemas.py" provides: "Cross-package Pydantic response models (D-10 destination for shared schemas)" contains: "class CloudConnectionOut" - path: "backend/api/cloud.py" provides: "Cloud endpoint router updated to import from api.schemas" contains: "from api.schemas import CloudConnectionOut" key_links: - from: "backend/api/cloud.py" to: "backend/api/schemas.py" via: "import statement at module top" pattern: "from api.schemas import CloudConnectionOut" - from: "backend/api/admin.py" to: "backend/api/schemas.py" via: "import statement to be added in plan 08-04 admin split" pattern: "from api.schemas import CloudConnectionOut" --- Create the new `backend/api/schemas.py` module and move `CloudConnectionOut` into it. Update `backend/api/cloud.py` to import from the new location. This MUST happen BEFORE plan 08-04 splits `api/admin.py` — otherwise the admin-split plan would break `cloud.py`'s `from api.admin import CloudConnectionOut` import. Purpose: Eliminate the cross-package coupling between `api/cloud.py` and `api/admin.py` (RESEARCH.md §"Pitfall 3"). Establishes the `api/schemas.py` module that plan 08-04 will continue populating with any models discovered to be shared. Output: New `backend/api/schemas.py` with `CloudConnectionOut`; one updated import in `backend/api/cloud.py`. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.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/cloud.py class CloudConnectionOut(BaseModel): id: str provider: str display_name: str status: str connected_at: datetime server_url: Optional[str] = None connection_username: Optional[str] = None model_config = {"from_attributes": True} @field_validator("id", mode="before") @classmethod def coerce_id_to_str(cls, v) -> str: return str(v) from api.admin import CloudConnectionOut from api.schemas import CloudConnectionOut Task 1: Create backend/api/schemas.py with CloudConnectionOut backend/api/schemas.py - backend/api/admin.py lines 198-223 (current CloudConnectionOut definition — must be copied verbatim including the field_validator) - backend/api/__init__.py (confirm it exists as a package marker; do not modify) - .planning/phases/08-stack-upgrade-backend-decomposition/08-PATTERNS.md section "backend/api/schemas.py" (exact pattern template) Create `backend/api/schemas.py` as a new file. Add a `from __future__ import annotations` header. Add a module docstring: `"""Cross-package Pydantic response models. Models here are used by 2+ API packages and cannot live in a single package without creating circular imports (D-10, RESEARCH.md Pitfall 3)."""`. Add imports: `from datetime import datetime`, `from typing import Optional`, `from pydantic import BaseModel, field_validator`. Then define `class CloudConnectionOut(BaseModel)` with the exact field set from `backend/api/admin.py` lines ~198-223 (per D-10): `id: str`, `provider: str`, `display_name: str`, `status: str`, `connected_at: datetime`, `server_url: Optional[str] = None`, `connection_username: Optional[str] = None`, `model_config = {"from_attributes": True}`, and the `@field_validator("id", mode="before") coerce_id_to_str` classmethod that returns `str(v)`. Add a class-level docstring noting SEC-08: `credentials_enc` deliberately excluded, moved from `api/admin.py`, used by `api/cloud.py` and `api/admin/`. Do NOT remove the class from `api/admin.py` in this task — task 2 handles the import switch in cloud.py, and plan 08-04 will delete the original definition during the admin split. cd backend && python -c "from api.schemas import CloudConnectionOut; obj = CloudConnectionOut.model_validate({'id': 'abc-123', 'provider': 'google_drive', 'display_name': 'Test', 'status': 'ACTIVE', 'connected_at': '2026-06-07T00:00:00'}); assert obj.id == 'abc-123' and obj.provider == 'google_drive'" - File `backend/api/schemas.py` exists - `grep -c "^class CloudConnectionOut" backend/api/schemas.py` returns 1 - `grep -c "coerce_id_to_str" backend/api/schemas.py` returns 1 - `grep -c "from_attributes" backend/api/schemas.py` returns 1 - `python -c "from api.schemas import CloudConnectionOut"` from inside `backend/` exits 0 with no output - All seven fields (id, provider, display_name, status, connected_at, server_url, connection_username) are present per `grep -c " [a-z_]*: " backend/api/schemas.py` ≥ 7 schemas.py exists, importable, CloudConnectionOut validates a sample dict, original admin.py class is still in place (deletion deferred to plan 08-04). Task 2: Switch backend/api/cloud.py import to api.schemas backend/api/cloud.py - backend/api/cloud.py line 35 (current `from api.admin import CloudConnectionOut`) - backend/api/cloud.py top imports block (confirm no other re-exports from `api.admin` exist) In `backend/api/cloud.py`, replace the single line `from api.admin import CloudConnectionOut` with `from api.schemas import CloudConnectionOut`. Do not change any other imports or any code below. After this edit, run the full test suite to confirm cloud endpoints still respond with the identical JSON shape (existing `tests/test_cloud.py` covers admin response surface; if not, the broader `pytest -v` smoke covers the admin list-cloud-connections endpoint too). cd backend && grep -c "from api.admin import CloudConnectionOut" api/cloud.py; cd backend && grep -c "from api.schemas import CloudConnectionOut" api/cloud.py; cd backend && pytest tests/test_cloud.py -x -v 2>&1 | tail -20 - `grep -c "from api.admin import CloudConnectionOut" backend/api/cloud.py` returns 0 - `grep -c "from api.schemas import CloudConnectionOut" backend/api/cloud.py` returns 1 - `cd backend && pytest tests/test_cloud.py -x` exits 0 - `cd backend && pytest tests/test_admin.py -x -k "cloud"` exits 0 (admin endpoints that serialize CloudConnectionOut continue to work because the original definition in admin.py is still untouched at this point) cloud.py imports CloudConnectionOut from api.schemas; tests for cloud endpoints and any admin endpoints that surface cloud connections continue to pass. ## Trust Boundaries | Boundary | Description | |----------|-------------| | api/cloud.py → api/schemas.py | Module-load-time import; no runtime data crosses this boundary other than a class reference | | Admin list-cloud-connections endpoint → CloudConnectionOut serialization | SEC-08 requires `credentials_enc` to be deliberately excluded from this model | ## STRIDE Threat Register | Threat ID | Category | Component | Disposition | Mitigation Plan | |-----------|----------|-----------|-------------|-----------------| | T-08-02-01 | Information Disclosure | CloudConnectionOut schema | mitigate | Field list must NOT include `credentials_enc`; field set is restricted to the 7 fields documented in PATTERNS.md | | T-08-02-02 | Tampering | id coercion | mitigate | Preserve `@field_validator("id", mode="before") coerce_id_to_str` so UUID→str conversion behavior is byte-identical to the old definition | | T-08-02-03 | Denial of Service | Duplicate class definitions | mitigate | Plan 08-04 removes the old definition from api/admin.py during the admin split; until then both definitions coexist but only the schemas.py version is imported by cloud.py | | T-08-02-SC | Supply Chain | No new packages | accept | This plan installs zero new packages | - `cd backend && pytest tests/test_cloud.py tests/test_admin.py -x` — zero failures - `grep -rn "from api.admin import CloudConnectionOut" backend/` returns no matches (cloud.py was the only consumer) - `grep -rn "from api.schemas import CloudConnectionOut" backend/` returns exactly one match (cloud.py) - `backend/api/admin.py` still contains the original `class CloudConnectionOut` definition (deletion deferred to plan 08-04) - `backend/api/schemas.py` exists with `CloudConnectionOut` defined correctly - `backend/api/cloud.py` imports from `api.schemas` - Cloud and admin tests continue to pass - No other file is modified Create `.planning/phases/08-stack-upgrade-backend-decomposition/08-02-SUMMARY.md` when done. Include: confirmation that the original class in admin.py is left untouched, the exact `pytest tests/test_cloud.py tests/test_admin.py -v` summary line, and a note that plan 08-04 owns the deletion of the old admin.py definition.