# Phase 13: Virtual-Local Cloud Operations - Research **Researched:** 2026-06-22 **Domain:** Provider-neutral cloud mutations, authorized cloud open/preview, connection health recovery, and shared browser UX **Confidence:** HIGH ## User Constraints Deliver owner-authorized connection maintenance and the main cloud file-management operations through the same shared browser interactions used for local files: test/reconnect/disconnect, open/preview, upload, create folder, rename, move within one connection, and delete. Mutations must respect normalized provider capabilities, promptly reconcile metadata, refresh affected listings, and emit metadata-only audit events. Phase 14 owns temporary byte-cache lifecycle and analysis; permanent cloud-to-local import remains future requirement `IMPORT-01`. - **D-01:** Cloud open, preview, and upload must feel the same as local storage and reuse the same shared components, progress presentation, and interaction paths. `CloudFolderView` remains a thin data provider; cloud-specific transport does not justify parallel UI. - **D-02:** Preview stays inside DocuVault and must not trigger a browser-to-device download. Unsupported preview formats fall back to an ownership-checked authorized download; provider credentials and raw provider URLs are never exposed. - **D-03:** A same-name upload opens a conflict dialog and never overwrites silently. The available decisions are Keep both with a renamed item, Replace where supported, Skip, and Cancel all. - **D-04:** Multi-file uploads run as a sequential queue. A conflict or error pauses the whole queue for user input without losing remaining items. Conflict actions are Keep both, Replace, Skip, and Cancel all; error actions are Retry, Skip, and Cancel all. Retry/Keep both/Replace/Skip resume the queue, while Cancel all stops it. - **D-05:** Create-folder and rename collisions automatically choose a non-conflicting human-readable counter name: `Report (1).pdf`, `Report (2).pdf`, or `Projects (1)` for folders. - **D-06:** If a concurrent client takes the candidate name between checking and mutation, retry with the next counter within a small bounded number of attempts. - **D-07:** Rename, move, or delete must not operate on stale metadata when the item changed externally. Stop the mutation, refresh the affected folder, explain what changed, and ask the user to retry. - **D-08:** Moving cloud items matches local storage: support both drag-to-folder and the shared folder picker. Destinations are restricted to the same cloud connection; cross-provider transfer remains out of scope. - **D-09:** Invalid folder destinations, including the folder itself and its descendants, are disabled before submission and independently rejected by the backend. - **D-10:** Delete confirmation is context-sensitive. Files receive a standard confirmation; folders clearly warn that nested contents are included. - **D-11:** Prefer the provider's trash/recycle-bin operation when supported. When only permanent deletion is available, the confirmation must say so explicitly. - **D-12:** Connection health appears in both places users need it: compact status plus Reconnect in the cloud browser, and full diagnostics plus Test/Reconnect controls in Settings. - **D-13:** Test automatically after connect/reconnect and after credential-related failures, and allow an explicit Test action. Do not probe the provider on every folder navigation. - **D-14:** A successful reconnect keeps cached metadata visible as stale, invalidates provider/listing/capability caches, and immediately refreshes the current folder. - **D-15:** A timeout or temporarily unreachable provider is an unhealthy connection state only. Preserve credentials and cached metadata, show an actionable warning, and allow retry/reconnect; never delete data because of transient failure. - **D-16:** Explicit user-initiated Disconnect requires confirmation, removes credentials and connection-scoped cloud metadata, and leaves all provider files untouched. Phase 13 has no byte cache to migrate. Out of scope for this phase: - Phase 14 temporary preview-byte cache and eviction behavior. - `IMPORT-01`: preserve cached/downloaded files as quota-counted local documents on explicit disconnect, under a connection-named folder with the provider hierarchy retained. ## Phase Requirements | Req ID | Requirement | Locked interpretation for Phase 13 | |---|---|---| | CONN-01 | User can connect, reconnect, test, and disconnect each supported cloud provider. | Add explicit test and reconnect flows without creating a parallel cloud UX. | | CONN-02 | User can see connection health and actionable errors for expired, revoked, or invalid credentials. | Surface compact browser status and fuller Settings diagnostics with controlled provider-error mapping. | | CONN-03 | Reconnecting or refreshing credentials invalidates stale provider caches without exposing credentials. | Reconnect must preserve metadata rows as stale, invalidate cache layers, and refresh current browse state. | | CLOUD-02 | User can open and preview supported cloud documents through DocuVault authorization. | Preview stays in-app; fallback download is still DocuVault-authorized and never a raw provider URL. | | CLOUD-03 | User can upload files into the currently viewed cloud folder. | Use the shared upload queue and conflict-resolution flow already implied by local UX. | | CLOUD-04 | User can create folders in connected cloud storage where the provider supports it. | Automatic collision suffixing plus bounded retry on concurrent races. | | CLOUD-05 | User can rename cloud files and folders where the provider supports it. | Same counter-suffix policy as create; stale metadata must stop-and-refresh, not force. | | CLOUD-06 | User can move files and folders within the same cloud connection where the provider supports it. | Shared picker and drag-move UX; reject self/descendant and cross-connection moves in UI and backend. | | CLOUD-07 | User can delete cloud files and folders after explicit confirmation. | Prefer trash/recycle-bin where supported; confirmation must disclose permanent-delete providers. | | CLOUD-09 | Successful cloud mutations update navigation promptly and produce metadata-only audit events. | Mutations must reconcile `cloud_items`, refresh folder state, and write same-transaction metadata-only audit rows. | ## Project Constraints (from AGENTS.md) - `StorageBrowser.vue` remains the single file browser; `CloudFolderView.vue` and `FileManagerView.vue` stay thin data providers. [VERIFIED: AGENTS.md] - No router-local duplicate helpers. Shared helpers belong in the existing module map: `backend/deps/utils.py`, `backend/storage/exceptions.py`, `backend/services/auth.py`, `backend/storage/cloud_base.py`, `backend/services/cloud_items.py`, `backend/api/cloud/schemas.py`. [VERIFIED: AGENTS.md] - Service layer raises domain errors or `ValueError`, never `HTTPException`; routers translate them. [VERIFIED: AGENTS.md] - Cloud browse and refresh must remain metadata-only and must not download provider bytes or mutate quota. [VERIFIED: AGENTS.md; VERIFIED: `backend/tests/test_cloud_security.py`] - `reconcile_cloud_listing` remains the only metadata reconciliation entry point; provider backends must not update `cloud_items` rows directly. [VERIFIED: AGENTS.md; VERIFIED: `backend/services/cloud_items.py`] - JWT access token stays in Pinia memory only; refresh token stays in `httpOnly` Strict cookie only. Any new cloud endpoints must preserve the existing auth model. [VERIFIED: AGENTS.md] - Every new endpoint, store path, service function, and shared component behavior must ship with tests, and backend/frontend suites must pass before the phase is complete. [VERIFIED: AGENTS.md] - Security gates remain mandatory: ownership checks on every resource path, CSRF protection on all state-changing endpoints, no credential leakage, SSRF allowlisting for WebDAV/Nextcloud, metadata-only audit logs, and no admin access to document content. [VERIFIED: AGENTS.md] ## Summary Phase 13 should extend the existing cloud browse foundation rather than branch around it. The codebase already has the right structural spine: a normalized `CloudResourceAdapter` vocabulary, owner-scoped connection-ID browse routes, centralized `cloud_items` reconciliation, a shared `StorageBrowser`, connection health statuses in Settings, and provider SDK wrappers that already know how to upload/download/delete bytes at the backend boundary. The missing work is the orchestration layer that turns those primitives into safe, provider-neutral cloud mutations and authorized open/preview flows. [VERIFIED: `backend/storage/cloud_base.py`; VERIFIED: `backend/api/cloud/browse.py`; VERIFIED: `backend/services/cloud_items.py`; VERIFIED: `frontend/src/components/storage/StorageBrowser.vue`; VERIFIED: `frontend/src/views/CloudFolderView.vue`] The highest-leverage implementation is to add a Phase 13 mutation/content contract adjacent to `CloudResourceAdapter`, keep provider-specific behavior in the backend adapters, and route all open/preview/mutate actions through owner-scoped connection-ID endpoints that reconcile metadata and emit audit rows in the same transaction. That preserves Phase 12’s normalized model, avoids any provider-specific Vue branches, and keeps raw provider IDs, URLs, tokens, and download links off the client. [VERIFIED: `backend/services/audit.py`; VERIFIED: `backend/db/models.py`; CITED: Google Drive and Microsoft Graph content/download docs] Two execution risks need explicit planning attention. First, OAuth reconnect currently creates a new connection row rather than reauthorizing an existing one, which conflicts with D-14 and CONN-03. Second, OneDrive token refresh is currently in-memory only, so successful mutation/open flows can silently depend on credentials that are never persisted back to `cloud_connections.credentials_enc`. Both issues are Phase 13 blockers for trustworthy reconnect and mutation semantics. [VERIFIED: `backend/api/cloud/connections.py`; VERIFIED: `backend/storage/onedrive_backend.py`] **Primary recommendation:** Implement Phase 13 as one provider-neutral cloud operations layer: `connection-id API -> service orchestration -> mutable cloud adapter -> reconcile/audit/freshness update -> shared StorageBrowser`, with reconnect and token-refresh persistence treated as Wave 1 platform work before UI polish. ## Architectural Responsibility Map | Capability | Primary Tier | Secondary Tier | Rationale | |---|---|---|---| | Connection test/reconnect/disconnect | Backend API + service orchestration | SettingsCloudTab / CloudFolderView | Health truth and credential mutation are server-owned; UI only presents state and user intent. | | Open / preview / authorized download | Backend content endpoint | StorageBrowser action surface | Browser must never receive raw provider URLs or credentials. | | Upload queue + conflict UI | StorageBrowser + CloudFolderView | Backend mutation service | Queue pause/resume is shared UX state; actual upload and conflict truth come from backend/provider. | | Create / rename / move / delete semantics | Mutable cloud adapter + cloud operations service | StorageBrowser | Provider differences belong in adapters; shared UI should consume normalized outcomes. | | Metadata reconciliation after mutation | `backend/services/cloud_items.py` | Celery refresh task | Stable IDs and freshness semantics already live here; do not duplicate in routers or adapters. | | Connection capability / health refresh | Provider adapter | cloudConnections Pinia store | Adapter knows scope/reauth/offline truth; store caches the server result for browser/settings reuse. | | Metadata-only audit events | Backend API/service transaction | Admin audit UI | Existing `write_audit_log()` helper already fits the phase requirement. | | Security enforcement | FastAPI deps + provider validators | Tests | Ownership, CSRF, SSRF, and secrecy are backend invariants, not UI conventions. | ## Standard Stack ### Core | Library | Version | Purpose | Why Standard | |---|---|---|---| | FastAPI | 0.128.8 | Owner-scoped cloud API routes and response schemas | Already the project’s canonical API framework; adding Phase 13 endpoints here avoids split auth behavior. [VERIFIED: `backend/requirements.txt`] | | SQLAlchemy async | 2.0.49 | Atomic metadata reconciliation and audit writes | Existing ORM layer already owns `cloud_items`, `cloud_folder_states`, `cloud_connections`, and `audit_log`. [VERIFIED: `backend/requirements.txt`] | | google-api-python-client | 2.197.0 | Google Drive move/rename/delete/content operations | Already installed and used by the Drive backend; no new SDK needed. [VERIFIED: `backend/requirements.txt`] | | msal | 1.37.0 | OneDrive/Graph token handling | Already installed and wrapped by the OneDrive backend. [VERIFIED: `backend/requirements.txt`] | | webdavclient3 | 3.14.7 | WebDAV/Nextcloud PUT/MKCOL/MOVE/DELETE primitives | Already installed and used by both DAV adapters. [VERIFIED: `backend/requirements.txt`] | | Vue | 3.5.38 | Shared browser flows in the existing frontend | Existing thin-view + smart-component architecture already matches the phase rules. [VERIFIED: `frontend/package.json`] | | Vitest | 4.1.7 | Frontend interaction and rendered-flow regression tests | Already pinned and used across cloud/browser tests. [VERIFIED: `frontend/package.json`] | | pytest | 9.0.3 | Backend provider/API/security contract tests | Already pinned and used across cloud suites. [VERIFIED: `backend/requirements.txt`] | ### Supporting | Library | Version | Purpose | When to Use | |---|---|---|---| | httpx | 0.28.1 | Async integration/API tests and provider HTTP boundaries | Keep for endpoint tests and mocked provider transports. [VERIFIED: `backend/requirements.txt`] | | Celery | 5.6.3 | Folder refresh after reconnect or stale-metadata recovery | Reuse for background refresh only; Phase 13 should not introduce separate async machinery. [VERIFIED: `backend/requirements.txt`] | | Pinia | 2.1.0 | Cloud browse/health state and upload-queue coordination | Keep queue state and server freshness centralized, but keep provider semantics in backend APIs. [VERIFIED: `frontend/package.json`] | ### Alternatives Considered | Instead of | Could Use | Tradeoff | |---|---|---| | Existing provider SDKs | New abstraction package or sync client library | Adds risk and duplicates code the repo already carries. | | Authorized backend preview/download | Browser-direct provider links | Violates D-02 and the project’s credential/privacy boundary. | | Shared StorageBrowser extension | Cloud-only grid or modal stack | Violates the “looks the same to the user => same code” rule. | **Installation:** ```bash # None — Phase 13 should reuse the repository's existing pinned stack. ``` **Version verification:** No new external packages are recommended in this research. The versions above are the repository’s pinned execution versions from `backend/requirements.txt` and `frontend/package.json`, which is sufficient for Phase 13 planning because the recommendation is to stay within the existing stack. [VERIFIED: repository pins] ## Package Legitimacy Audit No external package install is recommended for Phase 13. | Package | Registry | Age | Downloads | Source Repo | Verdict | Disposition | |---|---|---:|---:|---|---|---| | none | — | — | — | — | OK | Reuse existing pinned dependencies only | **Packages removed due to [SLOP] verdict:** none **Packages flagged as suspicious [SUS]:** none ## Architecture Patterns ### System Architecture Diagram ```mermaid flowchart TD U["User action in shared StorageBrowser"] --> V["CloudFolderView / SettingsCloudTab
(thin data providers)"] V --> API["FastAPI cloud endpoints
connection-id scoped"] API --> S["Cloud operations service
validate ownership, stale state, conflict policy"] S --> A["Mutable cloud adapter
Google / OneDrive / Nextcloud / WebDAV"] A --> P["Provider API / WebDAV server"] S --> R["reconcile_cloud_listing / folder freshness"] S --> L["write_audit_log(metadata only)"] R --> API API --> V V --> B["StorageBrowser props
items, capabilities, queue, health"] B --> U S -. refresh after reconnect / stale mismatch .-> C["Celery refresh_cloud_folder"] C --> A ``` ### Recommended Project Structure ```text backend/ ├── api/cloud/ │ ├── browse.py # existing read path │ ├── connections.py # existing connect/rename/disconnect path │ ├── operations.py # Phase 13 mutate/open/preview/test endpoints │ └── schemas.py # extend with mutation/result payloads only ├── services/ │ ├── cloud_items.py # existing reconciliation/freshness source of truth │ ├── cloud_operations.py # Phase 13 orchestration / stale checks / audit wiring │ └── audit.py # existing metadata-only audit helper ├── storage/ │ ├── cloud_base.py # extend with mutable adapter contract │ ├── google_drive_backend.py │ ├── onedrive_backend.py │ ├── nextcloud_backend.py │ └── webdav_backend.py frontend/src/ ├── views/CloudFolderView.vue # keep thin; swap placeholders for API/store handlers ├── components/storage/StorageBrowser.vue ├── stores/cloudConnections.js # add health + queue state, not provider logic └── api/cloud.js # add Phase 13 client methods ``` ### Pattern 1: Provider-neutral mutation results **What:** Add a mutable cloud adapter contract that returns normalized outcomes such as `updated_item`, `affected_parent_refs`, `conflict`, `stale`, `reauth_required`, and `used_trash`, rather than leaking provider response shapes into routers or Vue. **When to use:** Every create/rename/move/delete/upload/open/preview/test operation. **Example:** ```python # Pattern adapted from official provider docs and current DocuVault contracts. result = await adapter.rename_item( connection_id=conn.id, user_id=user.id, provider_item_id=item.provider_item_id, target_name=candidate_name, if_match=item.etag, ) ``` **Why:** Google Drive, Graph, and WebDAV all expose different verbs and conflict signals, but the UI only needs normalized outcomes. [CITED: Google Drive `files.update`; CITED: Microsoft Graph `driveItem-update`; CITED: RFC 4918 MOVE/Overwrite] ### Pattern 2: Reconcile after mutate, not before response only **What:** Every successful mutation should update the provider first, then reconcile local metadata and folder freshness in the same request transaction before returning. **When to use:** Upload, create folder, rename, move, delete, reconnect refresh. **Example:** ```python provider_result = await adapter.delete_item(...) await apply_mutation_reconciliation(session, provider_result) await write_audit_log(session, event_type="cloud.item.deleted", ...) ``` **Why:** `cloud_items` owns stable row identity and browse correctness. Returning success before reconcile creates stale navigation and violates CLOUD-09. [VERIFIED: `backend/services/cloud_items.py`; VERIFIED: `backend/services/audit.py`] ### Pattern 3: Sequential shared upload queue with pause reasons **What:** Keep queue state in the cloud view/store, but treat each conflict or provider error as a paused queue state that requires an explicit next action. **When to use:** Multi-file upload from the shared StorageBrowser. **Example:** ```javascript // Queue state belongs in shared UI flow, not provider code. queue = [{ file, state: 'running' | 'paused_conflict' | 'paused_error' | 'done' }] ``` **Why:** D-03 and D-04 are user-experience rules, not provider rules. The backend should return normalized conflict/error responses; the shared browser should decide whether to resume, skip, retry, or cancel all. [VERIFIED: `frontend/src/components/storage/StorageBrowser.vue`; VERIFIED: `frontend/src/views/FileManagerView.vue`] ### Anti-Patterns to Avoid - **Cloud-only browser layout:** violates the locked single-browser rule and will drift from local behavior. - **Provider-specific route parameters in Vue:** keep using connection UUID + opaque `provider_item_id`; never split or derive paths client-side. - **Raw provider download URLs in responses:** violates D-02 and leaks provider internals. - **Blind overwrite on rename/upload/create:** violates D-03, D-05, D-06, and provider conditional-write semantics. - **Reconnect by creating a new connection row:** breaks CONN-03 and D-14 because cached metadata and stable navigation become orphaned. ## Don't Hand-Roll | Problem | Don't Build | Use Instead | Why | |---|---|---|---| | Provider auth / token dance | Custom OAuth or refresh logic | Existing Google/MSAL + current backend wrappers | The repo already carries these SDKs and their edge cases. | | WebDAV mutation semantics | Ad hoc HTTP verbs assembled in routers | Existing WebDAV backend methods plus RFC-compliant headers | MOVE/MKCOL/DELETE/PUT conflict behavior is subtle. | | Audit pipeline | New cloud-only audit table | Existing `services.audit.write_audit_log()` | Same-transaction metadata logging already exists. | | Shared file UI | New cloud grid/dialog system | `StorageBrowser.vue` + thin provider views | Project rule forbids parallel code for same-looking UX. | | Client-side stale detection | Heuristics in Vue | Backend etag/version preconditions + refresh results | Only the backend has trustworthy provider state. | | Permanent preview cache | New Phase-13 cache subsystem | Minimal authorized open/preview hydration now; Phase 14 owns lifecycle | Prevents scope bleed into CACHE-03/04/05. | **Key insight:** Phase 13 is not a package-selection problem; it is a contract-extension problem. The codebase already has the right libraries, so hand-rolled divergence is a bigger risk than missing dependencies. ## Common Pitfalls ### Pitfall 1: Google Drive scope looks writable but is still visibility-limited **What goes wrong:** The app can mutate only files within the `drive.file` visibility boundary, so “browse all of My Drive and mutate anything” fails even though the SDK calls are correct. **Why it happens:** `drive.file` is least-privilege and only covers files the user has opened with or created via the app. [CITED: Google Drive API scopes] **How to avoid:** Treat scope limitations as a first-class capability/health outcome, and decide explicitly whether Phase 13 should preserve `drive.file` or require a broader scope upgrade with user consent. **Warning signs:** Items appear in browse flows inconsistently, capability state flips to reauth/scope warnings, or open/mutate actions fail only on pre-existing files. ### Pitfall 2: OneDrive refresh succeeds once but future requests regress **What goes wrong:** A request refreshes the access token in memory, but the persisted encrypted credentials remain stale, so later requests or workers fail again. **Why it happens:** The current backend refresh helper updates runtime state but does not persist the new credential set. [VERIFIED: `backend/storage/onedrive_backend.py`] **How to avoid:** Return refreshed credentials from the adapter/service boundary and persist them atomically when a request or reconnect succeeds. **Warning signs:** Health check passes immediately after reconnect, then later background refresh or a second request returns `REQUIRES_REAUTH`. ### Pitfall 3: WebDAV overwrite rules differ from local expectations **What goes wrong:** MOVE/rename/create behavior overwrites or conflicts differently across servers. **Why it happens:** WebDAV uses protocol-level overwrite semantics, not local filesystem UX defaults. `Overwrite: F` must return `412 Precondition Failed` when the destination exists. [CITED: RFC 4918] **How to avoid:** Normalize create/rename/move through explicit collision probing or conditional requests and convert provider responses into Keep-both / Replace / Skip / Retry UI outcomes. **Warning signs:** Same-name moves unexpectedly replace files, or rename conflicts surface as generic 500/409 errors without a resumable queue state. ### Pitfall 4: Preview leaks provider internals **What goes wrong:** The browser receives a raw Drive/Graph/WebDAV URL or provider download token. **Why it happens:** Provider SDKs often expose “downloadUrl” conveniences that are tempting to forward. [CITED: Microsoft Graph `driveItem` resource] **How to avoid:** Keep preview/open/download as backend-authorized proxy or streaming endpoints and redact provider-only details from all responses. **Warning signs:** Frontend code stores provider URLs, `window.open()` targets third-party hosts directly, or logs include download URLs. ### Pitfall 5: Shared browser queue and provider mutation truth get split **What goes wrong:** The UI invents local queue conflict decisions that the backend/provider never confirmed. **Why it happens:** Local UX seems simple, but cloud conflicts can depend on provider state, scope, etag, and stale metadata. **How to avoid:** Make the backend authoritative for conflict/stale/offline classification and let the shared browser only orchestrate the user’s next action. **Warning signs:** Keep-both names diverge from what the provider actually created, or retry resumes without a fresh backend decision. ## Code Examples Verified patterns from official sources: ### Google Drive move within one parent graph ```python # Source pattern: https://developers.google.com/workspace/drive/api/reference/rest/v3/files/update # Drive files have a single parent; moves are addParents/removeParents, not path rewrites. await drive.files().update( fileId=file_id, addParents=new_parent_id, removeParents=old_parent_id, body={}, ).execute() ``` ### Microsoft Graph safe rename / move with precondition ```python # Source pattern: https://learn.microsoft.com/en-us/graph/api/driveitem-update?view=graph-rest-1.0 # Use PATCH and send If-Match when etag is known so stale items fail safely. await graph.patch( f"/me/drive/items/{item_id}", headers={"If-Match": etag}, json={"name": new_name, "parentReference": {"id": dest_id}}, ) ``` ### WebDAV conflict-aware move ```python # Source pattern: RFC 4918 MOVE with Overwrite: F # Existing destination should yield 412, which maps cleanly to a Keep-both/Replace prompt. MOVE source -> destination Headers: Destination: Overwrite: F ``` ## State of the Art | Old Approach | Current Approach | When Changed | Impact | |---|---|---|---| | Separate local/cloud browser logic | One shared browser with normalized item shape and capabilities | Phase 12 / 12.1 | Phase 13 should extend shared events, not create cloud-only UI. [VERIFIED: Phase 12/12.1 artifacts] | | “Health” inferred from a successful browse response | Explicit freshness/health state from backend plus connection status | Phase 12.1 | Reconnect/test flows should preserve stale metadata instead of clearing state. [VERIFIED: `backend/services/cloud_items.py`; VERIFIED: `backend/api/cloud/browse.py`] | | Provider-specific direct content links | Authorized backend-mediated open/preview/download | Modern cloud SaaS security norm | Keeps provider credentials and raw URLs off the client. [CITED: Graph content/download model; CITED: Drive export/download model] | | N+1 WebDAV-style metadata fetches | Prefer one authoritative browse/mutate contract and conditional operations | Current provider reliability direction | Reduces stale/conflict ambiguity and makes provider differences testable. [VERIFIED: current code; CITED: Nextcloud WebDAV basic ops; RFC 4918] | **Deprecated/outdated:** - Treating OAuth reconnect as “add another account” when the user intends to repair an existing connection. This no longer matches the locked D-14/D-16 behavior. - Treating frontend timestamps or HTTP 200 alone as proof of provider freshness. Phase 12.1 explicitly moved freshness truth to the backend. ## Assumptions Log | # | Claim | Section | Risk if Wrong | |---|---|---|---| | A1 | Phase 13 should preserve the current Google Drive `drive.file` scope unless the planner/user explicitly chooses a broader consent surface. | Common Pitfalls / Security Domain | Medium — some user-visible mutations may be impossible on previously existing Drive items. | | A2 | Reconnect for OAuth providers should update an existing `cloud_connections` row rather than create a replacement row. | Summary / Architecture Patterns | High — wrong choice breaks stable navigation, cache invalidation, and metadata continuity. | | A3 | Preview/open can be implemented with authorized backend hydration now without introducing the full persistent cache lifecycle reserved for Phase 14. | Summary / Don’t Hand-Roll | Medium — if the implementation implicitly requires durable cache semantics, scope bleeds into Phase 14. | ## Open Questions (RESOLVED) 1. **Google Drive scope — RESOLVED:** Request broader Google Drive access for Phase 13 UX parity rather than retaining `drive.file`. Consent copy and security tests must explicitly cover the expanded scope. [USER DECISION: 2026-06-22; CITED: Google Drive API scopes] 2. **OAuth reconnect model — RESOLVED:** Use a connection-ID reconnect intent whose OAuth state identifies and patches the existing owned `cloud_connections` row, preserving stable metadata identity while invalidating provider/listing/capability caches. [AGENT DISCRETION; VERIFIED: D-14 and current callback behavior] 3. **Upload queue payload — RESOLVED:** Use typed JSON conflict/error bodies with stable `kind` and `reason` codes; keep pause/resume queue state in the shared frontend flow and do not introduce resumable operation tokens in Phase 13. [AGENT DISCRETION; VERIFIED: D-03/D-04] 4. **Preview matrix — RESOLVED:** Phase 13 supports only supported binary file preview. Google Workspace export preview and Microsoft Office-native rendering/editing are excluded; unsupported formats use the authorized download fallback. A future phase will integrate Collabora in a separate internally accessible container. [USER DECISION: 2026-06-22] ## Environment Availability | Dependency | Required By | Available | Version | Fallback | |---|---|---|---|---| | `docker` / `docker compose` | Backend integration/security runs and service-backed validation | ✓ | Docker 29.5.3 | — | | `node` | Frontend Vitest runs | ✓ | v26.3.1 | — | | `npm` | Frontend scripts | ✓ | 11.16.0 | — | | `python3` (host) | Ad hoc local scripts only | ✓ | 3.9.6 | Use containerized backend for project Python 3.12 behavior | | `pytest` (host) | Direct host backend test execution | ✗ | — | Run backend tests in the backend container or a project venv | **Missing dependencies with no fallback:** - none **Missing dependencies with fallback:** - Host `pytest` is unavailable; use `docker compose run --rm backend pytest ...` or a project-local venv. - Host Python is 3.9.6 while the project target is Python 3.12; use the backend container for execution-fidelity checks. ## Validation Architecture ### Test Framework | Property | Value | |---|---| | Framework | Backend: `pytest 9.0.3` + `pytest-asyncio 1.4.0`; Frontend: `vitest 4.1.7` | | Config file | Backend: none explicit in repo root; Frontend: Vite/Vitest defaults via `frontend/package.json` | | Quick run command | Backend: `docker compose run --rm backend pytest -v tests/test_cloud.py tests/test_cloud_security.py -x` ; Frontend: `cd frontend && npm run test -- src/views/__tests__/CloudFolderView.test.js src/views/__tests__/CloudFolderRenderedFlow.test.js src/components/storage/__tests__/StorageBrowser.capabilities.test.js` | | Full suite command | `docker compose run --rm backend pytest -v` and `cd frontend && npm run test` | ### Phase Requirements → Test Map | Req ID | Behavior | Test Type | Automated Command | File Exists? | |---|---|---|---|---| | CONN-01 | connect, reconnect, explicit test, disconnect for each provider | backend integration + frontend component/store | `docker compose run --rm backend pytest -v tests/test_cloud.py -k "connect or disconnect or reconnect or test"` | ✅ extend `backend/tests/test_cloud.py`; ✅ extend `frontend/src/components/settings/__tests__/SettingsCloudTab.test.js` | | CONN-02 | actionable connection health for expired/revoked/invalid creds | backend integration + frontend component | `docker compose run --rm backend pytest -v tests/test_cloud.py tests/test_cloud_security.py -k "reauth or invalid or health"` | ✅ extend existing suites | | CONN-03 | reconnect invalidates caches without exposing creds | backend integration + security + store | `docker compose run --rm backend pytest -v tests/test_cloud.py tests/test_cloud_security.py -k "cache or credential"` | ✅ extend `backend/tests/test_cloud.py`; ✅ extend `frontend/src/stores/__tests__/cloudConnections.test.js` | | CLOUD-02 | authorized open/preview/download with no raw provider URLs | backend API/security + rendered-flow | `docker compose run --rm backend pytest -v tests/test_cloud.py tests/test_cloud_security.py -k "open or preview or content"` | ❌ Wave 0 add dedicated backend content tests; ✅ extend rendered-flow suites | | CLOUD-03 | upload into current cloud folder with sequential queue + conflict handling | backend integration + frontend view/component | `docker compose run --rm backend pytest -v tests/test_cloud.py -k "upload"` and `cd frontend && npm run test -- src/views/__tests__/CloudFolderView.test.js` | ✅ existing files to extend; ❌ Wave 0 add queue/conflict suite | | CLOUD-04 | create folder with keep-both suffix + bounded retry | backend provider contract + integration | `docker compose run --rm backend pytest -v tests/test_cloud_backends.py tests/test_cloud.py -k "create_folder"` | ❌ Wave 0 add mutation contract cases | | CLOUD-05 | rename file/folder with stale protection and suffixing | backend provider contract + integration + rendered-flow | `docker compose run --rm backend pytest -v tests/test_cloud_backends.py tests/test_cloud.py -k "rename"` | ❌ Wave 0 add rename mutation suite | | CLOUD-06 | move within same connection, reject self/descendant/cross-connection | backend provider contract + security + frontend interaction | `docker compose run --rm backend pytest -v tests/test_cloud.py tests/test_cloud_security.py -k "move"` | ❌ Wave 0 add move suite; ✅ extend `StorageBrowser.dragmove` coverage if needed | | CLOUD-07 | delete with explicit confirmation and trash/permanent semantics | backend provider contract + integration + frontend component | `docker compose run --rm backend pytest -v tests/test_cloud.py tests/test_cloud_security.py -k "delete"` | ❌ Wave 0 add delete mutation suite | | CLOUD-09 | prompt navigation refresh and metadata-only audit log on success | backend integration + audit assertion + rendered-flow | `docker compose run --rm backend pytest -v tests/test_cloud.py -k "audit or refresh"` | ❌ Wave 0 add audit-specific cloud mutation assertions | ### Sampling Rate - **Per task commit:** backend targeted cloud suite + frontend targeted cloud suite for the touched behavior - **Per wave merge:** `docker compose run --rm backend pytest -v tests/test_cloud.py tests/test_cloud_backends.py tests/test_cloud_provider_contract.py tests/test_cloud_security.py tests/test_cloud_items.py` and `cd frontend && npm run test` - **Phase gate:** Full backend suite green, full frontend suite green, then security/dependency gates before `$gsd-verify-work` ### Wave 0 Gaps - [ ] `backend/tests/test_cloud_mutations.py` — provider-neutral mutation contract for create/rename/move/delete/upload/open/preview result shapes - [ ] `backend/tests/test_cloud_reconnect.py` or equivalent expansion in `test_cloud.py` — connection-ID reconnect semantics, token persistence, cache invalidation, metadata retention - [ ] `backend/tests/test_cloud_audit.py` or equivalent mutation assertions in `test_cloud.py` — metadata-only audit rows for each successful mutation - [ ] `frontend/src/components/storage/__tests__/StorageBrowser.cloud-queue.test.js` — sequential cloud upload queue, conflict pause, error pause, resume/cancel-all - [ ] `frontend/src/views/__tests__/CloudFolderOpenPreview.test.js` — cloud open/preview/download action behavior through shared browser - [ ] `frontend/src/components/settings/__tests__/SettingsCloudTab.health.test.js` — explicit Test and Reconnect controls, transient outage vs reauth UI - [ ] Host backend test runner gap: use containerized pytest until a project-local Python 3.12 venv is provisioned ## Security Domain ### Applicable ASVS Categories | ASVS Category | Applies | Standard Control | |---|---|---| | V2 Authentication | yes | Existing JWT + httpOnly refresh-cookie auth; no provider credentials exposed to client | | V3 Session Management | yes | Existing token rotation/revocation; reconnect/open endpoints must preserve same auth boundary | | V4 Access Control | yes | `resolve_owned_connection`, resource ownership checks, admin-negative tests | | V5 Input Validation | yes | Pydantic/FastAPI request schemas plus opaque provider-ref handling | | V6 Cryptography | yes | Existing encrypted `credentials_enc` via `cryptography`; no custom crypto | ### Known Threat Patterns for this stack | Pattern | STRIDE | Standard Mitigation | |---|---|---| | IDOR on connection or cloud item mutation | Elevation of Privilege | Resolve by connection UUID under current user; reject foreign rows with indistinguishable not-found behavior | | Raw provider URL / token leakage in preview/download | Information Disclosure | Backend-authorized proxy/stream only; never return `downloadUrl`, access tokens, or `credentials_enc` | | SSRF through Nextcloud/WebDAV server URL or redirects | Tampering | Reuse `validate_cloud_url`, normalize Nextcloud URLs centrally, and revalidate redirect/host boundaries | | CSRF on state-changing cloud endpoints | Tampering | Existing SameSite Strict cookie + Origin/Referer validation on every mutate/reconnect/disconnect route | | Stale-etag mutation or concurrent overwrite | Tampering | Conditional provider writes when supported; on mismatch return controlled stale result and refresh folder | | Cross-connection move | Tampering | UI restrict destination tree to one connection and backend enforces same-connection invariant | | Audit log leakage of provider secrets or paths | Information Disclosure | Metadata-only `write_audit_log()` payloads with stable IDs/names/status only | | Queue confusion causing silent overwrite | Repudiation / Tampering | Conflict responses must be explicit and resumable; no silent replace path | | Temporary outage treated as destructive disconnect | Denial of Service | Preserve credentials and cached metadata on transient failure; only explicit disconnect purges state | ## Sources ### Primary (HIGH confidence) - Internal code and tests reviewed directly: - `AGENTS.md` - `.planning/ROADMAP.md` - `.planning/REQUIREMENTS.md` - `.planning/PROJECT.md` - `.planning/STATE.md` - `.planning/phases/13-virtual-local-cloud-operations/13-CONTEXT.md` - `.planning/phases/12-cloud-resource-foundation/12-RESEARCH.md` - `.planning/phases/12.1-fix-nextcloud-root-listing-and-sync-visibility/12.1-RESEARCH.md` - `backend/storage/cloud_base.py` - `backend/api/cloud/browse.py` - `backend/api/cloud/connections.py` - `backend/services/cloud_items.py` - `backend/services/audit.py` - `backend/storage/google_drive_backend.py` - `backend/storage/onedrive_backend.py` - `backend/storage/webdav_backend.py` - `backend/storage/nextcloud_backend.py` - `frontend/src/components/storage/StorageBrowser.vue` - `frontend/src/views/CloudFolderView.vue` - `frontend/src/components/settings/SettingsCloudTab.vue` - `backend/tests/test_cloud.py` - `backend/tests/test_cloud_provider_contract.py` - `backend/tests/test_cloud_security.py` - `backend/tests/test_cloud_capabilities.py` - `frontend/src/views/__tests__/CloudFolderView.test.js` - `frontend/src/views/__tests__/CloudFolderRenderedFlow.test.js` - `frontend/src/components/storage/__tests__/StorageBrowser.capabilities.test.js` - `frontend/src/components/settings/__tests__/SettingsCloudTab.test.js` - Google Drive API scopes: [developers.google.com/workspace/drive/api/guides/api-specific-auth](https://developers.google.com/workspace/drive/api/guides/api-specific-auth) - Google Drive `files` resource: [developers.google.com/workspace/drive/api/reference/rest/v3/files](https://developers.google.com/workspace/drive/api/reference/rest/v3/files) - Google Drive `files.update`: [developers.google.com/workspace/drive/api/reference/rest/v3/files/update](https://developers.google.com/workspace/drive/api/reference/rest/v3/files/update) - Google Drive `files.delete`: [developers.google.com/workspace/drive/api/reference/rest/v3/files/delete](https://developers.google.com/workspace/drive/api/reference/rest/v3/files/delete) - Google Drive `files.export`: [developers.google.com/workspace/drive/api/reference/rest/v3/files/export](https://developers.google.com/workspace/drive/api/reference/rest/v3/files/export) - Google Drive `files.download`: [developers.google.com/workspace/drive/api/reference/rest/v3/files/download](https://developers.google.com/workspace/drive/api/reference/rest/v3/files/download) - Microsoft Graph `driveItem` resource: [learn.microsoft.com/en-us/graph/api/resources/driveitem?view=graph-rest-1.0](https://learn.microsoft.com/en-us/graph/api/resources/driveitem?view=graph-rest-1.0) - Microsoft Graph create folder: [learn.microsoft.com/en-us/graph/api/driveitem-post-children?view=graph-rest-1.0](https://learn.microsoft.com/en-us/graph/api/driveitem-post-children?view=graph-rest-1.0) - Microsoft Graph rename/move update: [learn.microsoft.com/en-us/graph/api/driveitem-update?view=graph-rest-1.0](https://learn.microsoft.com/en-us/graph/api/driveitem-update?view=graph-rest-1.0) - Microsoft Graph move: [learn.microsoft.com/en-us/graph/api/driveitem-move?view=graph-rest-1.0](https://learn.microsoft.com/en-us/graph/api/driveitem-move?view=graph-rest-1.0) - Microsoft Graph delete: [learn.microsoft.com/en-us/graph/api/driveitem-delete?view=graph-rest-1.0](https://learn.microsoft.com/en-us/graph/api/driveitem-delete?view=graph-rest-1.0) - Microsoft Graph get content: [learn.microsoft.com/en-us/graph/api/driveitem-get-content?view=graph-rest-1.0](https://learn.microsoft.com/en-us/graph/api/driveitem-get-content?view=graph-rest-1.0) - Nextcloud WebDAV basic ops: [docs.nextcloud.com/server/latest/developer_manual/client_apis/WebDAV/basic.html](https://docs.nextcloud.com/server/latest/developer_manual/client_apis/WebDAV/basic.html) - RFC 4918 WebDAV: [rfc-editor.org/rfc/rfc4918](https://www.rfc-editor.org/rfc/rfc4918) ### Secondary (MEDIUM confidence) - README and Docker Compose runtime contracts for local execution and service availability. ### Tertiary (LOW confidence) - none ## Metadata **Confidence breakdown:** - Standard stack: HIGH - no new dependencies are recommended; all proposed tooling is already pinned in-repo. - Architecture: HIGH - recommendations align with existing Phase 12/12.1 contracts and current code seams. - Pitfalls: HIGH - most are verified directly in current code or official provider docs, with assumptions explicitly logged. **Research date:** 2026-06-22 **Valid until:** 2026-07-06 ## RESEARCH COMPLETE