Files

8.4 KiB

phase, plan, subsystem, tags, dependency_graph, tech_stack, key_files, decisions, metrics
phase plan subsystem tags dependency_graph tech_stack key_files decisions metrics
12 02 cloud-browse
cloud
browse
celery
idor
stale-while-revalidate
provider-normalization
requires provides affects
12-01
GET /api/cloud/connections/{connection_id}/items
PATCH /api/cloud/connections/{id}
tasks.cloud_tasks.refresh_cloud_folder
CloudResourceAdapter mixin on all 4 providers
backend/api/cloud/ (new package)
backend/services/cloud_items.py
backend/tasks/cloud_tasks.py
added patterns
CloudResourceAdapter abstract mixin (storage/cloud_base.py)
refresh_cloud_folder Celery task (tasks/cloud_tasks.py)
api/cloud/ package with connections.py, browse.py, schemas.py
stale-while-revalidate (5-min threshold, background Celery refresh)
sentinel exception pattern for Celery retry in sync layer
whitelisted Pydantic response schemas (credential-free)
UUID coerce-to-uuid.UUID pattern for SQLAlchemy UUID(as_uuid=True) columns
created modified
backend/api/cloud/__init__.py
backend/api/cloud/browse.py
backend/api/cloud/connections.py
backend/api/cloud/schemas.py
backend/tasks/cloud_tasks.py
AGENTS.md
backend/storage/google_drive_backend.py
backend/storage/onedrive_backend.py
backend/storage/webdav_backend.py
backend/storage/nextcloud_backend.py
backend/storage/cloud_backend_factory.py
backend/services/cloud_items.py
backend/db/models.py
backend/celery_app.py
backend/main.py
frontend/package.json
backend/tests/test_cloud.py
backend/tests/test_cloud_items.py
backend/tests/conftest.py
README.md
UUID coercion in cloud_items.py service layer: always converts to uuid.UUID objects for WHERE clause binding with UUID(as_uuid=True) columns; test_cloud_items.py updated to use UUID(as_uuid=True) instead of String(36) patch to match production ORM
Stale-while-revalidate threshold set to 5 minutes: returns cached rows immediately on first browse if stale, schedules background refresh
refresh_cloud_folder: decrypts credentials inside worker, never in broker payload; 3-retry bounded backoff 30s/90s/270s with ±10s jitter
Celery retry sentinel pattern: _TransientProviderError and _TerminalProviderError escape asyncio.run() to Celery sync layer
display_name_override ORM field added to CloudConnection model (migration 0006 already had the column)
duration completed tasks_completed files_changed
~4 hours (continued from prior session) 2026-06-18T21:17:38Z 3 19

Phase 12 Plan 02: Cloud Resource Foundation Browse API Summary

Connection-ID browse endpoint with all four providers implementing CloudResourceAdapter, stale-while-revalidate caching, and durable refresh_cloud_folder Celery task.

What Was Built

Task 1: Provider normalization (committed ff33439) All four cloud providers (Google Drive, OneDrive, WebDAV, Nextcloud) now implement CloudResourceAdapter as a mixin. Each provider adds list_folder() and get_capabilities() returning normalized CloudListing and capability dicts. Factory adds build_cloud_resource_adapter(). 22 new tests in test_cloud_backends.py verify pagination, no-byte-download, and capability evidence contracts.

Task 2: api/cloud/ package decomposition (committed e186019) Replaced flat api/cloud.py with a package:

  • connections.py: all connection management routes (OAuth, WebDAV, list, delete, rename)
  • browse.py: canonical GET /api/cloud/connections/{connection_id}/items
  • schemas.py: credential-free CloudBrowseResponse, CloudItemOut, FolderFreshnessOut
  • __init__.py: aggregates sub-routers under /api/cloud prefix

Fixed UUID type incompatibility: test_cloud_items.py now uses UUID(as_uuid=True) matching the production ORM instead of String(36) patch. Service layer coerces all inputs to uuid.UUID objects for WHERE clauses.

Task 3: Celery task + version bump (committed c6237ca)

  • refresh_cloud_folder Celery task: idempotent, bounded 3-retry (30s/90s/270s ± 10s jitter), credential decryption inside worker
  • Stale-while-revalidate: 5-minute threshold in browse endpoint
  • Version bumped 0.1.4 → 0.1.5 in backend/main.py and frontend/package.json
  • AGENTS.md created/updated with Phase 12 state and new shared module map entries
  • README.md updated with connection-ID browse API table

Commits

Hash Message
ff33439 feat(12-02): normalize all four providers into CloudResourceAdapter contract
e186019 feat(12-02): decompose api/cloud.py into package with connection-ID browse endpoint
c6237ca feat(12-02): add refresh_cloud_folder Celery task, staleness trigger, version 0.1.5

Test Results

493 passing, 1 pre-existing failure (test_extract_docx — missing python-docx module, unrelated to this plan), 6 skipped, 7 xfailed.

New tests added:

  • 22 in test_cloud_backends.py (Task 1)
  • 11 in test_cloud.py (Task 2: IDOR, admin block, credential exclusion, rename, duplicate providers, malformed UUID)
  • 5 in test_cloud_items.py (Task 3: idempotency, cached-row retention, task structure, no-byte-download, scheduling)

Deviations from Plan

Auto-fixed Issues

1. [Rule 1 - Bug] UUID type incompatibility between test fixtures

  • Found during: Task 2 debugging
  • Issue: test_cloud_items.py patched UUID columns to String(36) while conftest used UUID(as_uuid=True). Service functions could not work with both simultaneously — UUID objects caused InterfaceError on String(36) columns; strings caused AttributeError (.hex) on UUID(as_uuid=True) columns.
  • Fix: Updated test_cloud_items.py fixture to use UUID(as_uuid=True) approach (matching production ORM). Updated all helpers to use uuid.UUID objects. Updated cloud_items.py service to always coerce to uuid.UUID objects in WHERE clauses.
  • Files modified: backend/tests/test_cloud_items.py, backend/services/cloud_items.py, backend/tests/conftest.py
  • Commits: e186019, c6237ca

2. [Rule 2 - Missing functionality] display_name_override missing from ORM model

  • Found during: Task 2
  • Issue: Migration 0006 added display_name_override column to cloud_connections table but ORM model didn't declare it, causing AttributeError when PATCH endpoint tried to set it.
  • Fix: Added display_name_override: Mapped[Optional[str]] = mapped_column(Text, nullable=True) to CloudConnection model.
  • Files modified: backend/db/models.py
  • Commit: e186019

3. [Rule 1 - Bug] Celery patch target wrong in refresh scheduling test

  • Found during: Task 3 test authoring
  • Issue: Test patched api.cloud.browse.refresh_cloud_folder but browse.py uses a local import inside the function body, so the module attribute doesn't exist at test time.
  • Fix: Changed patch target to tasks.cloud_tasks.refresh_cloud_folder.
  • Files modified: backend/tests/test_cloud.py
  • Commit: c6237ca

4. [Rule 1 - Bug] should_refresh condition never triggered for stale cached items

  • Found during: Task 3 scheduling test
  • Issue: Browse endpoint's should_refresh only checked for no items, "refreshing" state, or last_refreshed_at is None. Cached items with refresh_state="fresh" and a recent last_refreshed_at would never schedule background refresh.
  • Fix: Added 5-minute staleness check — if last_refreshed_at is older than 5 minutes, refresh is scheduled.
  • Files modified: backend/api/cloud/browse.py
  • Commit: c6237ca

Known Stubs

None — all data sources wired. Browse endpoint returns real durable cached rows from cloud_items table and schedules real Celery task.

Threat Flags

Flag File Description
threat_flag: credential_in_worker backend/tasks/cloud_tasks.py Credentials decrypted inside Celery worker — master key must be in CLOUD_CREDS_KEY env var on all worker nodes. Never log credentials. Already mitigated by existing key-from-env pattern.

Self-Check: PASSED

Files confirmed created:

  • backend/api/cloud/__init__.py ✓
  • backend/api/cloud/browse.py ✓
  • backend/api/cloud/connections.py ✓
  • backend/api/cloud/schemas.py ✓
  • backend/tasks/cloud_tasks.py ✓

Commits confirmed:

Test result: 493 passed, 1 pre-existing failure ✓