---
phase: "12"
plan: "02"
type: execute
wave: 2
depends_on:
  - "12-01"
files_modified:
  - backend/storage/cloud_backend_factory.py
  - backend/storage/google_drive_backend.py
  - backend/storage/onedrive_backend.py
  - backend/storage/nextcloud_backend.py
  - backend/storage/webdav_backend.py
  - backend/api/cloud/__init__.py
  - backend/api/cloud/connections.py
  - backend/api/cloud/browse.py
  - backend/api/cloud/schemas.py
  - backend/api/cloud.py
  - backend/tasks/cloud_tasks.py
  - backend/celery_app.py
  - backend/tests/test_cloud_backends.py
  - backend/tests/test_cloud.py
  - backend/tests/test_cloud_items.py
  - backend/main.py
  - frontend/package.json
  - frontend/package-lock.json
  - AGENTS.md
  - README.md
autonomous: true
requirements:
  - CONN-04
  - CLOUD-01
  - CLOUD-08
  - CACHE-01
  - CACHE-02
  - SYNC-01

must_haves:
  truths:
    - "D-01/D-02/D-03: API lists distinct connection roots and browses by owned connection UUID, including duplicate provider types"
    - "D-08: capability reasons come from provider metadata, granted permissions, and safe read-only checks only"
    - "D-11/D-12: previously indexed folders return durable rows promptly and refresh in the background"
    - "D-13/D-14: successful refresh reconciles silently; failures retain cached rows, warning state, last-success time, and bounded retries"
    - "D-18/CACHE-01: browse and refresh never download file bytes, write MinIO, or mutate quota"
  artifacts:
    - path: "backend/api/cloud/browse.py"
      provides: "Owner-scoped connection-ID browse response and refresh scheduling"
      contains: "connection_id"
    - path: "backend/tasks/cloud_tasks.py"
      provides: "Idempotent cloud metadata refresh with bounded retries"
      contains: "refresh_cloud_folder"
    - path: "backend/api/cloud/schemas.py"
      provides: "Whitelisted capability, item, freshness, and connection schemas"
      contains: "CloudBrowseResponse"
    - path: "backend/tests/test_cloud.py"
      provides: "Multiple account, IDOR/admin, credential exclusion, and no-byte browse tests"
      contains: "test_browse_connection_rejects_foreign_owner"
  key_links:
    - from: "backend/api/cloud/browse.py"
      to: "backend/services/cloud_items.py"
      via: "owned connection resolution and durable cached listing"
      pattern: "list_cloud_children"
    - from: "backend/tasks/cloud_tasks.py"
      to: "backend/storage/cloud_backend_factory.py"
      via: "connection provider selects CloudResourceAdapter after ownership lookup"
      pattern: "build_cloud_resource_adapter"
---

<objective>
Normalize all four providers into the Phase 12 resource contract and expose durable, owner-scoped, connection-ID browsing with safe stale-while-revalidate behavior. Decompose the cloud router without changing its public connection-management behavior.

Scope fence: capability reporting is active, but create/rename/move/delete/open/preview implementations remain Phase 13; file byte hydration and cache accounting remain Phase 14.
</objective>

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

<context>
@.planning/phases/12-cloud-resource-foundation/12-01-SUMMARY.md
@.planning/phases/12-cloud-resource-foundation/12-CONTEXT.md
@.planning/phases/12-cloud-resource-foundation/12-RESEARCH.md
@.planning/phases/12-cloud-resource-foundation/12-PATTERNS.md
@.planning/phases/12-cloud-resource-foundation/12-VALIDATION.md
</context>

<tasks>

<task type="auto">
  <name>Task 1: Normalize provider listings and capability evidence</name>
  <files>backend/storage/cloud_backend_factory.py, backend/storage/google_drive_backend.py, backend/storage/onedrive_backend.py, backend/storage/nextcloud_backend.py, backend/storage/webdav_backend.py, backend/tests/test_cloud_backends.py</files>
  <read_first>
    - backend/storage/cloud_base.py — contract created by Plan 12-01
    - backend/storage/cloud_backend_factory.py — existing lazy provider construction
    - backend/api/cloud.py — current inline Google/OneDrive listing implementations to relocate
    - backend/storage/nextcloud_backend.py — current N+1 listing implementation and SSRF guard
    - backend/tests/test_cloud_backends.py — provider contract test conventions
  </read_first>
  <action>
    Make each provider expose `CloudResourceAdapter` behavior and normalize all pages into `CloudListing`. Google requests explicit files fields for id, parents, name, mimeType, size, modifiedTime, version/etag-equivalent fields, and item capabilities; follows `nextPageToken`; represents native Google documents with nullable/zero size without treating them as folders; and reports `insufficient_scope` when current `drive.file` credentials cannot browse intended content. OneDrive follows `@odata.nextLink` and normalizes id, drive/parent reference, file/folder facet, size, modified time, eTag/cTag, and effective permission state. Nextcloud/WebDAV use SSRF-validated non-mutating PROPFIND/OPTIONS metadata, normalize getetag/getlastmodified/getcontentlength/getcontenttype/resourcetype, and treat absent properties as unknown rather than unsupported. Factory adds `build_cloud_resource_adapter`. Fixture tests assert equivalent normalized results, complete pagination, safe capability evidence, and zero byte-download/mutation calls.
  </action>
  <acceptance_criteria>
    - all supported providers return the same normalized resource and capability schema
    - Google and OneDrive pagination is followed before `complete=true`
    - every WebDAV/Nextcloud outbound request retains `validate_cloud_url()` protection
    - provider tests verify list/capability flows never call get_object/put/delete/move/rename
    - `pytest -q tests/test_cloud_backends.py` exits 0
  </acceptance_criteria>
  <verify><automated>cd backend && pytest -q tests/test_cloud_backends.py</automated></verify>
  <done>All four providers pass the normalized metadata/capability and complete-pagination contract without byte or mutation calls.</done>
</task>

<task type="auto">
  <name>Task 2: Decompose cloud API and add connection-ID browse schemas</name>
  <files>backend/api/cloud/__init__.py, backend/api/cloud/connections.py, backend/api/cloud/browse.py, backend/api/cloud/schemas.py, backend/api/cloud.py, backend/main.py, backend/tests/test_cloud.py</files>
  <read_first>
    - backend/api/cloud.py — preserve all connection/OAuth/WebDAV/default-storage behavior while splitting
    - backend/api/documents/__init__.py — no-prefix sub-router aggregator pattern and FastAPI empty-path warning
    - backend/api/schemas.py — explicit response whitelist pattern
    - backend/deps/utils.py — canonical `parse_uuid` and `get_client_ip`
    - backend/services/cloud_items.py — owner-scoped service API
  </read_first>
  <action>
    Convert `api/cloud.py` into an `api/cloud/` package with one parent router owning `/api/cloud`; sub-routers carry no prefix. Preserve existing endpoint URLs as compatibility routes where unambiguous, but introduce canonical `GET /api/cloud/connections/{connection_id}/items` with optional encoded parent reference and owner-scoped `PATCH /api/cloud/connections/{connection_id}` accepting only a validated nonblank `display_name` field. Parse UUIDs with `deps.utils.parse_uuid`, resolve the connection by current owner, and return explicit schemas for connection root, items, merged capabilities, freshness state, and last refresh timestamp. List connections keeps every same-provider account and never returns credentials. For an indexed folder, return durable rows and set/schedule refresh; for an empty first visit, perform one bounded metadata fetch so the user receives content. Map service/provider errors to controlled router errors. Admin remains blocked by `get_regular_user`. Add tests for renaming one connection without affecting another, duplicate providers, foreign connection/item IDs, admin access, credentials/token/password exclusion, malformed UUID, mass-assignment rejection, and compatibility endpoint behavior.
  </action>
  <acceptance_criteria>
    - cloud package aggregator is the only router with `/api/cloud` prefix
    - canonical browse endpoint uses connection UUID and cannot select by provider alone
    - response schemas cannot serialize `credentials_enc` or decrypted credential fields
    - two active Google Drive connections for one user are independently browsable and independently renameable
    - focused cloud API tests pass
  </acceptance_criteria>
  <verify><automated>cd backend && pytest -q tests/test_cloud.py -k "connection or browse or credential or admin"</automated></verify>
  <done>Canonical connection-ID browse and display-name APIs preserve existing connection flows and pass ownership/credential negatives.</done>
</task>

<task type="auto">
  <name>Task 3: Add durable metadata refresh task, retry behavior, and plan documentation</name>
  <files>backend/tasks/cloud_tasks.py, backend/celery_app.py, backend/tests/test_cloud_items.py, backend/tests/test_cloud.py, backend/main.py, frontend/package.json, frontend/package-lock.json, AGENTS.md, README.md</files>
  <read_first>
    - backend/tasks/document_tasks.py — sync Celery entry point bridging async service with bounded retry
    - backend/celery_app.py — JSON-only task registration and queue routing
    - backend/services/cloud_items.py — idempotent reconciliation/folder-state functions
    - AGENTS.md — documentation, version, testing, and security protocol
    - README.md — cloud feature and API documentation sections
  </read_first>
  <action>
    Add `refresh_cloud_folder(user_id, connection_id, parent_ref)` as a JSON-only Celery task on the documents queue. The task opens its own async DB session, revalidates owner/connection, decrypts credentials inside the worker (never broker payload), marks refreshing, obtains a complete normalized listing, reconciles idempotently, and marks fresh. Retry only transient provider/network failures using bounded increasing countdowns with jitter or the established explicit retry harness; terminal auth/scope errors become warning state with safe reason/remedy. Update task registration. Add tests for idempotency, retry countdown bounds, failed refresh retaining rows/last-success time, and no provider byte calls/quota changes. Apply project protocol for this user-visible/API plan: bump backend/frontend/package-lock patch version from 0.1.4 to 0.1.5, update AGENTS current state/shared module map if new canonical modules were added, and document the connection-ID browse/capability foundation in README.
  </action>
  <acceptance_criteria>
    - task payload contains IDs/references only and no credential or provider byte content
    - transient retries are bounded; auth/scope failures do not retry forever
    - warning state preserves cached rows and last successful refresh timestamp
    - browse/refresh tests assert no `get_object`, MinIO put, or quota update
    - versions are exactly 0.1.5 in backend main, package.json, and package-lock root/package entry
    - focused backend tests pass
  </acceptance_criteria>
  <verify><automated>cd backend && pytest -q tests/test_cloud.py tests/test_cloud_backends.py tests/test_cloud_items.py && cd ../frontend && npm run build</automated></verify>
  <done>Durable refresh retries safely, retains cached metadata on failure, performs no byte/quota work, and plan documentation/version 0.1.5 is complete.</done>
</task>

</tasks>

<threat_model>
| Threat ID | Threat | Mitigation and evidence |
|-----------|--------|-------------------------|
| T-12-01 | Connection/item IDOR | `get_regular_user` plus owner-scoped connection UUID lookup; foreign/admin negative tests |
| T-12-03 | Credential disclosure | Explicit schemas and broker payload tests exclude credentials/tokens/passwords |
| T-12-04 | WebDAV SSRF | Preserve URL validation before every request and redirect; regression tests |
| T-12-05 | Cache collision | Connection UUID replaces provider in route/task/durable identity |
| T-12-07 | Data loss from partial pagination | `complete=true` only after all pages; reconciliation test |
| T-12-08 | Provider error XSS/leak | Stable reason codes and controlled messages; no raw credential-bearing exception response |
| T-12-09 | Byte/quota side effects | Negative mocks assert metadata browse never invokes byte or quota APIs |
</threat_model>

<verification>
1. `cd backend && pytest -q tests/test_cloud.py tests/test_cloud_backends.py tests/test_cloud_items.py`
2. `cd backend && pytest -v`
3. `cd frontend && npm run build`
4. Inspect API response fixtures for zero credential fields.
5. Confirm Phase 13 mutation and Phase 14 byte-cache methods are absent.
</verification>

<success_criteria>
- Every provider supplies normalized metadata and capabilities.
- Browsing is keyed by owned connection UUID and supports duplicate provider accounts.
- Durable cached rows return without provider-byte downloads and refresh safely in background.
- Failure states preserve navigation, retry boundedly, and expose safe actionable reasons.
</success_criteria>

<output>Create `.planning/phases/12-cloud-resource-foundation/12-02-SUMMARY.md` when complete.</output>
