---
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"
---
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.
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
@.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
Task 1: Normalize provider listings and capability evidence
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
- 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
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.
- 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
cd backend && pytest -q tests/test_cloud_backends.py
All four providers pass the normalized metadata/capability and complete-pagination contract without byte or mutation calls.
Task 2: Decompose cloud API and add connection-ID browse schemas
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
- 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
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.
- 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
cd backend && pytest -q tests/test_cloud.py -k "connection or browse or credential or admin"
Canonical connection-ID browse and display-name APIs preserve existing connection flows and pass ownership/credential negatives.
Task 3: Add durable metadata refresh task, retry behavior, and plan documentation
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
- 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
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.
- 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
cd backend && pytest -q tests/test_cloud.py tests/test_cloud_backends.py tests/test_cloud_items.py && cd ../frontend && npm run build
Durable refresh retries safely, retains cached metadata on failure, performs no byte/quota work, and plan documentation/version 0.1.5 is complete.
| 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 |
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.
- 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.