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>
This commit is contained in:
curo1305
2026-06-23 20:34:57 +02:00
co-authored by Claude Sonnet 4.6
parent 73328eee0b
commit b67e77dd69
16 changed files with 3114 additions and 0 deletions
@@ -0,0 +1,524 @@
# 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