---
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"
---

<objective>
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`.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<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/cloud.py

<interfaces>
<!-- Key definition to move (from backend/api/admin.py lines ~198-223). -->
<!-- After this plan, this class lives ONLY in backend/api/schemas.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)

<!-- The import currently in backend/api/cloud.py line 35: -->
from api.admin import CloudConnectionOut

<!-- becomes: -->
from api.schemas import CloudConnectionOut
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Create backend/api/schemas.py with CloudConnectionOut</name>
  <files>backend/api/schemas.py</files>
  <read_first>
    - 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)
  </read_first>
  <action>
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.
  </action>
  <verify>
    <automated>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'"</automated>
  </verify>
  <acceptance_criteria>
    - 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
  </acceptance_criteria>
  <done>schemas.py exists, importable, CloudConnectionOut validates a sample dict, original admin.py class is still in place (deletion deferred to plan 08-04).</done>
</task>

<task type="auto">
  <name>Task 2: Switch backend/api/cloud.py import to api.schemas</name>
  <files>backend/api/cloud.py</files>
  <read_first>
    - 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)
  </read_first>
  <action>
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).
  </action>
  <verify>
    <automated>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</automated>
  </verify>
  <acceptance_criteria>
    - `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)
  </acceptance_criteria>
  <done>cloud.py imports CloudConnectionOut from api.schemas; tests for cloud endpoints and any admin endpoints that surface cloud connections continue to pass.</done>
</task>

</tasks>

<threat_model>
## 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 |
</threat_model>

<verification>
- `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)
</verification>

<success_criteria>
- `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
</success_criteria>

<output>
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.
</output>
