Files
curo1305andClaude Sonnet 4.6 b67e77dd69 docs(13): add phase 13 planning artifacts — virtual local cloud operations
Plans 01–11, CONTEXT, PATTERNS, RESEARCH, REVIEW-FIX, and updated
SUMMARY and CONTEXT for the virtual-local-cloud-operations phase.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 20:34:57 +02:00

525 lines
43 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 12s 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 projects 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 projects 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 repositorys 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<br/>(thin data providers)"]
V --> API["FastAPI cloud endpoints<br/>connection-id scoped"]
API --> S["Cloud operations service<br/>validate ownership, stale state, conflict policy"]
S --> A["Mutable cloud adapter<br/>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<br/>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 users 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: <target>
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 / Dont 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