Compare commits
4
Commits
e96137174f
...
0ddd983797
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ddd983797 | ||
|
|
b67e77dd69 | ||
|
|
73328eee0b | ||
|
|
ab31c1344c |
@@ -7,3 +7,5 @@ frontend/dist/
|
|||||||
frontend/package-lock.json
|
frontend/package-lock.json
|
||||||
frontend/stats.html
|
frontend/stats.html
|
||||||
screenshots/
|
screenshots/
|
||||||
|
.claire/
|
||||||
|
.claude/
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"plan_check": true,
|
"plan_check": true,
|
||||||
"verifier": true,
|
"verifier": true,
|
||||||
"nyquist_validation": true,
|
"nyquist_validation": true,
|
||||||
|
"use_worktrees": true,
|
||||||
"auto_advance": false,
|
"auto_advance": false,
|
||||||
"test_gate": true,
|
"test_gate": true,
|
||||||
"security_check": true,
|
"security_check": true,
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
# Debug: Phase 12 Cloud Schema Cold Start
|
|
||||||
|
|
||||||
**Status:** root cause found
|
|
||||||
**Date:** 2026-06-19
|
|
||||||
**UAT tests:** 1, 2
|
|
||||||
|
|
||||||
## Symptoms
|
|
||||||
|
|
||||||
- `GET /api/cloud/connections` raises `psycopg.errors.UndefinedColumn` for `cloud_connections.display_name_override`.
|
|
||||||
- `POST /api/cloud/connections/webdav` reaches `_upsert_cloud_connection` and raises the same error, preventing Nextcloud account connection.
|
|
||||||
|
|
||||||
## Root Cause
|
|
||||||
|
|
||||||
The running application code and ORM are at Phase 12, but the live PostgreSQL schema remains at Alembic revision `0005`.
|
|
||||||
|
|
||||||
Live evidence:
|
|
||||||
|
|
||||||
```text
|
|
||||||
$ docker compose run --rm backend alembic current
|
|
||||||
0005
|
|
||||||
```
|
|
||||||
|
|
||||||
Migration `backend/migrations/versions/0006_cloud_resource_foundation.py` correctly adds `cloud_connections.display_name_override` and the Phase 12 cloud metadata tables. The ORM correctly maps that column. The defect is that `docker-compose.yml` has no migration service and the backend command starts Uvicorn directly. Backend, Celery worker, and Celery beat depend on PostgreSQL health, but none depends on `alembic upgrade head` completing. Therefore `docker compose up` can run new application code against an old persistent database.
|
|
||||||
|
|
||||||
## Why Automated Tests Missed It
|
|
||||||
|
|
||||||
- Most backend tests build schema directly from `Base.metadata.create_all`, which validates the final ORM shape but bypasses Alembic history and deployment ordering.
|
|
||||||
- Existing Alembic tests target early migrations and do not exercise an existing PostgreSQL database upgraded from `0005` to `head`.
|
|
||||||
- Phase verification checked migration file structure and model parity, not a real cold-start Compose upgrade path.
|
|
||||||
|
|
||||||
## Files Involved
|
|
||||||
|
|
||||||
- `docker-compose.yml` — no one-shot migration service; backend/workers start after DB health only.
|
|
||||||
- `backend/migrations/versions/0006_cloud_resource_foundation.py` — contains the required column and tables but was not applied.
|
|
||||||
- `backend/db/models.py` — queries `display_name_override`, exposing schema drift immediately.
|
|
||||||
- `backend/tests/test_alembic.py` — lacks a 0005-to-head PostgreSQL upgrade regression.
|
|
||||||
- `.planning/phases/12-cloud-resource-foundation/12-VALIDATION.md` — cold-start test expected migration completion, but execution evidence did not actually verify Compose migration orchestration.
|
|
||||||
|
|
||||||
## Required Fix Direction
|
|
||||||
|
|
||||||
1. Add a one-shot Compose migration service that runs `alembic upgrade head` with `DATABASE_MIGRATE_URL` after PostgreSQL becomes healthy.
|
|
||||||
2. Make backend, Celery worker, and Celery beat wait for that migration service to complete successfully.
|
|
||||||
3. Add a PostgreSQL/Compose regression proving an existing revision `0005` advances to `0006/head` before the API starts, and assert `display_name_override`, `cloud_items`, `cloud_item_topics`, and `cloud_folder_states` exist.
|
|
||||||
4. Document the migration lifecycle and recovery command in README/RUNBOOK.
|
|
||||||
5. For the currently running environment, apply `alembic upgrade head` and restart application processes before resuming UAT.
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
Both UAT blockers share this root cause. No evidence currently indicates a separate Nextcloud credential or WebDAV defect.
|
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
---
|
||||||
|
phase: "12.1"
|
||||||
|
plan: "01"
|
||||||
|
type: tdd
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- backend/storage/cloud_base.py
|
||||||
|
- backend/storage/cloud_utils.py
|
||||||
|
- backend/storage/webdav_backend.py
|
||||||
|
- backend/storage/nextcloud_backend.py
|
||||||
|
- backend/storage/google_drive_backend.py
|
||||||
|
- backend/storage/onedrive_backend.py
|
||||||
|
- backend/storage/cloud_backend_factory.py
|
||||||
|
- backend/tests/test_cloud_provider_contract.py
|
||||||
|
- backend/tests/test_cloud_backends.py
|
||||||
|
- backend/tests/test_webdav_backend.py
|
||||||
|
- backend/tests/fixtures/cloud/nextcloud_root.xml
|
||||||
|
- backend/tests/fixtures/cloud/webdav_root.xml
|
||||||
|
- backend/tests/fixtures/cloud/google_drive_pages.json
|
||||||
|
- backend/tests/fixtures/cloud/onedrive_pages.json
|
||||||
|
- backend/main.py
|
||||||
|
- frontend/package.json
|
||||||
|
- frontend/package-lock.json
|
||||||
|
- AGENTS.md
|
||||||
|
- README.md
|
||||||
|
- RUNBOOK.md
|
||||||
|
- SECURITY.md
|
||||||
|
autonomous: true
|
||||||
|
requirements:
|
||||||
|
- CLOUD-01
|
||||||
|
- CACHE-01
|
||||||
|
- SYNC-01
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "Nextcloud accepts the exact four-argument CloudResourceAdapter.list_folder contract and returns CloudListing rather than a legacy list of dictionaries"
|
||||||
|
- "One shared contract suite runs against Nextcloud, generic WebDAV, Google Drive, and OneDrive"
|
||||||
|
- "Every provider normalizes root and nested children with trusted connection/user identity, opaque provider navigation identity, parent reference, kind, and nullable metadata"
|
||||||
|
- "A provider reports complete=True only after every page or complete DAV multistatus has been parsed successfully"
|
||||||
|
- "Browse contract tests prove zero byte-download and zero provider-mutation calls"
|
||||||
|
artifacts:
|
||||||
|
- path: "backend/tests/test_cloud_provider_contract.py"
|
||||||
|
provides: "Reusable four-provider adapter contract and forbidden-operation spies"
|
||||||
|
contains: "test_provider_list_folder_contract"
|
||||||
|
- path: "backend/storage/nextcloud_backend.py"
|
||||||
|
provides: "Nextcloud specialization that preserves the canonical WebDAV browse method"
|
||||||
|
contains: "NextcloudBackend"
|
||||||
|
- path: "backend/tests/fixtures/cloud/nextcloud_root.xml"
|
||||||
|
provides: "Credential-free DAV root fixture with folders, files, spaces, root self-entry, and optional metadata"
|
||||||
|
contains: "multistatus"
|
||||||
|
key_links:
|
||||||
|
- from: "backend/storage/nextcloud_backend.py"
|
||||||
|
to: "backend/storage/webdav_backend.py"
|
||||||
|
via: "inheritance or a narrow normalization hook; no incompatible public override"
|
||||||
|
pattern: 'class NextcloudBackend\(WebDAVBackend\)'
|
||||||
|
- from: "backend/tests/test_cloud_provider_contract.py"
|
||||||
|
to: "backend/storage/cloud_base.py"
|
||||||
|
via: "the same parametrized assertions for every provider factory"
|
||||||
|
pattern: "CloudListing|CloudResourceAdapter"
|
||||||
|
- from: "provider fixtures"
|
||||||
|
to: "CloudResource.provider_item_id"
|
||||||
|
via: "provider-specific response normalization"
|
||||||
|
pattern: "provider_item_id"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Repair the provider boundary test-first. Reproduce the Nextcloud signature failure, replace the legacy browse override with the shared adapter contract, and establish one reusable listing contract for all four providers with provider-specific root, nested, pagination, malformed-response, and no-byte/no-mutation fixtures.
|
||||||
|
|
||||||
|
This plan changes metadata listing only. It does not add upload, create-folder, rename, move, delete, preview, download, or byte caching.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||||
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@.planning/phases/12.1-fix-nextcloud-root-listing-and-sync-visibility/12.1-CONTEXT.md
|
||||||
|
@.planning/phases/12.1-fix-nextcloud-root-listing-and-sync-visibility/12.1-RESEARCH.md
|
||||||
|
@.planning/phases/12.1-fix-nextcloud-root-listing-and-sync-visibility/12.1-PATTERNS.md
|
||||||
|
@.planning/phases/12-cloud-resource-foundation/12-02-PLAN.md
|
||||||
|
@.planning/phases/12-cloud-resource-foundation/12-VALIDATION.md
|
||||||
|
@backend/storage/cloud_base.py
|
||||||
|
@backend/storage/nextcloud_backend.py
|
||||||
|
@backend/storage/webdav_backend.py
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Write the shared four-provider contract and red tests</name>
|
||||||
|
<files>backend/tests/test_cloud_provider_contract.py, backend/tests/test_cloud_backends.py, backend/tests/test_webdav_backend.py, backend/tests/fixtures/cloud/nextcloud_root.xml, backend/tests/fixtures/cloud/webdav_root.xml, backend/tests/fixtures/cloud/google_drive_pages.json, backend/tests/fixtures/cloud/onedrive_pages.json</files>
|
||||||
|
<read_first>
|
||||||
|
- backend/storage/cloud_base.py — exact CloudListing and CloudResource invariants
|
||||||
|
- backend/storage/cloud_backend_factory.py — supported-provider construction paths
|
||||||
|
- backend/tests/test_cloud_backends.py — existing provider mocks and missing behavioral coverage
|
||||||
|
- backend/tests/test_webdav_backend.py — existing webdavclient3 test conventions
|
||||||
|
- .planning/phases/12.1-fix-nextcloud-root-listing-and-sync-visibility/12.1-RESEARCH.md — confirmed signatures and provider fixture matrix
|
||||||
|
</read_first>
|
||||||
|
<action>
|
||||||
|
Create a reusable parametrized contract harness whose provider cases supply only fixture setup and response stubs. Add concrete tests named `test_provider_list_folder_contract`, `test_provider_root_and_nested_identity`, `test_provider_metadata_normalization`, `test_provider_consumes_all_pages_before_complete`, `test_provider_page_failure_is_incomplete`, and `test_provider_listing_never_downloads_or_mutates`. Invoke each adapter through the canonical positional/keyword signature `(connection_id, user_id, parent_ref=None, page_token=None)` and assert the result is CloudListing; this test must fail against the current Nextcloud override.
|
||||||
|
|
||||||
|
For every provider assert: trusted caller UUIDs are copied into resources; `provider_item_id` is the opaque navigation reference; `parent_ref` is the requested folder reference; `kind` is exactly file/folder; optional size/content-type/modified/version metadata becomes `None` when absent; and no provider response can override owner/connection IDs. Install spies that fail on GET-media/download/get_object/put/upload/create/move/copy/rename/delete methods and assert quota/MinIO are not imported or called by listing.
|
||||||
|
|
||||||
|
Add credential-free provider fixtures: Nextcloud and WebDAV Depth-1 multistatus with root self-entry, encoded spaces/unicode, collections, files, absent optional properties, absolute/relative hrefs, and a foreign/escaping href; Google Drive with root/nested queries, native files with nullable size, trashed exclusion, and multiple nextPageToken pages; OneDrive with root/nested endpoints, folder/file facets, encoded names, and multiple @odata.nextLink pages. Fixture names must be synthetic—not copied from the live account—and contain no URL, username, tokens, or provider-owned unexpected names.
|
||||||
|
</action>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- the Nextcloud canonical-signature regression fails before implementation and passes afterward
|
||||||
|
- the same six contract tests execute for nextcloud, webdav, google_drive, and onedrive
|
||||||
|
- fixtures cover root, nested, missing optional metadata, malformed/foreign data, and provider pagination
|
||||||
|
- no contract fixture or pytest ID contains credentials, a full live URL, or live unexpected names
|
||||||
|
- forbidden byte and mutation spies remain untouched for every provider
|
||||||
|
</acceptance_criteria>
|
||||||
|
<verify><automated>cd backend && pytest -q tests/test_cloud_provider_contract.py tests/test_cloud_backends.py tests/test_webdav_backend.py</automated></verify>
|
||||||
|
<done>The shared contract is red for the known Nextcloud violation and fully specifies normalized, complete, read-only behavior for all providers.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Repair Nextcloud/WebDAV listing without splitting the public contract</name>
|
||||||
|
<files>backend/storage/cloud_utils.py, backend/storage/webdav_backend.py, backend/storage/nextcloud_backend.py, backend/storage/cloud_backend_factory.py, backend/tests/test_cloud_provider_contract.py, backend/tests/test_webdav_backend.py</files>
|
||||||
|
<read_first>
|
||||||
|
- backend/storage/nextcloud_backend.py — incompatible legacy list_folder override to remove
|
||||||
|
- backend/storage/webdav_backend.py — canonical list_folder implementation and SSRF revalidation points
|
||||||
|
- backend/storage/cloud_utils.py — canonical URL/SSRF helpers
|
||||||
|
- backend/storage/cloud_backend_factory.py — credential-to-adapter construction
|
||||||
|
- backend/tests/test_cloud_security.py — existing SSRF invariants that must remain green
|
||||||
|
</read_first>
|
||||||
|
<action>
|
||||||
|
Remove the incompatible `NextcloudBackend.list_folder(folder_path="") -> list[dict]` public override. Let Nextcloud inherit the canonical WebDAV implementation, or add only a protected Nextcloud endpoint/path-normalization hook consumed by that implementation. Keep exactly one public DAV `list_folder` algorithm.
|
||||||
|
|
||||||
|
Normalize a Nextcloud base URL and username to its canonical DAV root in one pure shared helper used at adapter construction, while accepting an already canonical endpoint idempotently. Percent-encode the username as one path segment; preserve deployment subpaths; reject userinfo, query, fragment, non-HTTPS, malformed URLs, and URLs rejected by `validate_cloud_url`; never log the normalized URL because it contains the username path segment.
|
||||||
|
|
||||||
|
Disable automatic redirects in the DAV HTTP transport. If redirect support is required, follow it explicitly with a strict bounded hop count and, before every hop, parse and validate the target URL, require HTTPS and the original normalized origin, resolve the hostname, and reject loopback, private, link-local, multicast, unspecified, reserved, or changed/untrusted addresses. Revalidate immediately before dispatch to limit DNS rebinding. Add automated redirect tests for loopback, RFC1918/private, link-local, cross-origin, non-HTTPS, excessive-hop, and a permitted same-origin target; assert rejected targets receive no second request and no URL is leaked.
|
||||||
|
|
||||||
|
Make the shared DAV listing parser authoritative for one Depth-1 response: exclude exactly the normalized self href, preserve a canonical child provider reference, use DAV collection resource type for kind, decode display path segments exactly once, reject hrefs outside the configured root, and return complete=False on malformed/ambiguous responses. Do not silently convert per-item metadata failures into a complete listing. If the existing SDK cannot expose a safe one-response parser without a new dependency, retain its calls but enforce the same completeness and no-byte contract; document the tradeoff in the summary.
|
||||||
|
</action>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- `inspect.signature(NextcloudBackend.list_folder)` matches the canonical adapter method
|
||||||
|
- root and nested Nextcloud/WebDAV fixtures return normalized CloudListing values
|
||||||
|
- malformed XML, foreign hrefs, or interrupted metadata parsing cannot return complete=True
|
||||||
|
- Nextcloud URL normalization is idempotent and remains behind canonical SSRF validation
|
||||||
|
- automatic redirects are disabled or each bounded hop is same-origin and URL/DNS revalidated; loopback/private/link-local/cross-origin redirect tests pass
|
||||||
|
- no second public Nextcloud listing path or raw dictionary response remains
|
||||||
|
</acceptance_criteria>
|
||||||
|
<verify><automated>cd backend && pytest -q tests/test_cloud_provider_contract.py tests/test_webdav_backend.py tests/test_cloud_security.py -k 'provider or webdav or nextcloud or ssrf or redirect'</automated></verify>
|
||||||
|
<done>Nextcloud and generic WebDAV share one secure canonical listing path and the known signature defect is eliminated.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 3: Bring Drive and OneDrive to the same completion contract</name>
|
||||||
|
<files>backend/storage/google_drive_backend.py, backend/storage/onedrive_backend.py, backend/tests/test_cloud_provider_contract.py, backend/tests/test_cloud_backends.py</files>
|
||||||
|
<read_first>
|
||||||
|
- backend/storage/google_drive_backend.py — root query, nextPageToken, metadata and error behavior
|
||||||
|
- backend/storage/onedrive_backend.py — root/items endpoints, @odata.nextLink, metadata and error behavior
|
||||||
|
- backend/storage/cloud_base.py — complete/next_page_token semantics
|
||||||
|
- backend/tests/fixtures/cloud/google_drive_pages.json — required fixture cases
|
||||||
|
- backend/tests/fixtures/cloud/onedrive_pages.json — required fixture cases
|
||||||
|
</read_first>
|
||||||
|
<action>
|
||||||
|
Make Google Drive and OneDrive pass the same contract without creating provider-specific API schemas. Google Drive must translate root internally, follow every nextPageToken before complete=True, exclude trashed items, recognize folder MIME type, preserve native Google files with nullable size, and map controlled auth/scope failures without raw provider responses. OneDrive must translate root internally, follow every trusted @odata.nextLink, identify folders by facet, preserve nullable metadata, and reject cross-origin/untrusted continuation URLs. Any page failure returns complete=False with already normalized items retained and no deletion authority.
|
||||||
|
|
||||||
|
Keep provider IDs opaque. Do not turn DocuVault CloudItem UUIDs into provider references and do not call media/download endpoints. Extend concrete provider tests where behavior is too provider-specific for the shared assertions.
|
||||||
|
</action>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- all pages are consumed before complete=True for both providers
|
||||||
|
- page failure or untrusted continuation returns complete=False
|
||||||
|
- Drive native files and OneDrive folders normalize without fabricated metadata
|
||||||
|
- raw auth/provider bodies and continuation URLs are absent from raised/displayable messages
|
||||||
|
- the complete four-provider focused suite passes
|
||||||
|
</acceptance_criteria>
|
||||||
|
<verify><automated>cd backend && pytest -q tests/test_cloud_provider_contract.py tests/test_cloud_backends.py tests/test_webdav_backend.py tests/test_cloud_security.py</automated></verify>
|
||||||
|
<done>All four providers satisfy one listing/completeness/identity/no-byte contract with provider-specific pagination evidence.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 4: Document, security-review, commit, and push Plan 01</name>
|
||||||
|
<files>backend/main.py, frontend/package.json, frontend/package-lock.json, AGENTS.md, README.md, RUNBOOK.md, SECURITY.md, .planning/phases/12.1-fix-nextcloud-root-listing-and-sync-visibility/12.1-01-SUMMARY.md</files>
|
||||||
|
<action>Update the required docs for shipped Plan 01 behavior, including shared helpers and redirect policy. Because this repairs user-visible cloud browsing, bump the backend/frontend patch version once and keep package-lock metadata synchronized. Run a dedicated security agent against this plan's diff and resolve or escalate every finding. Re-run focused/full backend tests, Bandit and dependency gates applicable to the changed backend, `docker compose config --quiet`, and `git diff --check`. Stage only Plan 01 files, verify `.env` and unrelated changes are absent, create one atomic `fix(12.1): restore cloud adapter contract` commit, and push immediately according to AGENTS.md.</action>
|
||||||
|
<verify><automated>docker compose config --quiet && cd backend && pytest -v && bandit -r . && pip audit && cd .. && git diff --check</automated></verify>
|
||||||
|
<done>Plan 01 has its own docs, clean security-agent verdict, green gates, atomic commit, and push.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<threat_model>
|
||||||
|
| Threat ID | Threat | Mitigation and evidence |
|
||||||
|
|-----------|--------|-------------------------|
|
||||||
|
| T-12.1-01 | Provider response forges owner or connection identity | Contract copies trusted caller UUIDs and tests hostile fixture fields |
|
||||||
|
| T-12.1-02 | SSRF through Nextcloud normalization, redirects, or DAV hrefs | Canonical validator before construction/request; reject userinfo/query/fragment/private targets/escaping hrefs |
|
||||||
|
| T-12.1-03 | Partial page authorizes metadata deletion | complete=False on any page/parser/trust failure; contract tests every provider |
|
||||||
|
| T-12.1-04 | Browse downloads or mutates provider content | Failing spies on byte and mutation methods; metadata requests only |
|
||||||
|
| T-12.1-05 | Secrets/provider data leak through fixtures or failures | Synthetic fixtures, controlled exceptions, no live URL/user/token/unexpected-name output |
|
||||||
|
</threat_model>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
1. Observe the Nextcloud signature test fail before implementation and pass after repair.
|
||||||
|
2. Run the shared contract for all four providers, not four copied suites.
|
||||||
|
3. Run provider-specific root, nested, pagination, malformed-response, and no-byte tests.
|
||||||
|
4. Run existing cloud security tests to prove SSRF and credential boundaries remain intact.
|
||||||
|
5. Search confirms no incompatible Nextcloud public list_folder override remains.
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- Nextcloud can be invoked through CloudResourceAdapter exactly like every other provider.
|
||||||
|
- Every provider has executable evidence for normalized identity, kind, metadata, pagination/completeness, and read-only behavior.
|
||||||
|
- Provider failures cannot masquerade as an authoritative empty collection.
|
||||||
|
- No Phase 13 mutation or Phase 14 byte behavior enters the contract.
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>Create `.planning/phases/12.1-fix-nextcloud-root-listing-and-sync-visibility/12.1-01-SUMMARY.md` when complete.</output>
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
---
|
||||||
|
phase: "12.1"
|
||||||
|
plan: "02"
|
||||||
|
type: tdd
|
||||||
|
wave: 2
|
||||||
|
depends_on:
|
||||||
|
- "12.1-01"
|
||||||
|
files_modified:
|
||||||
|
- backend/services/cloud_items.py
|
||||||
|
- backend/api/cloud/browse.py
|
||||||
|
- backend/tasks/cloud_tasks.py
|
||||||
|
- backend/api/cloud/schemas.py
|
||||||
|
- backend/tests/test_cloud_items.py
|
||||||
|
- backend/tests/test_cloud.py
|
||||||
|
- backend/tests/test_cloud_security.py
|
||||||
|
- backend/main.py
|
||||||
|
- frontend/package.json
|
||||||
|
- frontend/package-lock.json
|
||||||
|
- AGENTS.md
|
||||||
|
- README.md
|
||||||
|
- RUNBOOK.md
|
||||||
|
- SECURITY.md
|
||||||
|
autonomous: true
|
||||||
|
requirements:
|
||||||
|
- CLOUD-01
|
||||||
|
- CACHE-01
|
||||||
|
- SYNC-01
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "CloudListing.complete=False can never transition a folder to fresh in either synchronous browse or Celery refresh"
|
||||||
|
- "A complete authoritative empty listing may become fresh and reconcile deletions"
|
||||||
|
- "Incomplete/error refresh retains cached children and the previous last successful refresh timestamp"
|
||||||
|
- "The synchronous API and background worker use one service-level reconciliation/completion gate"
|
||||||
|
- "Connection-ID ownership, credential secrecy, zero-byte browse, and zero quota mutation remain enforced"
|
||||||
|
artifacts:
|
||||||
|
- path: "backend/services/cloud_items.py"
|
||||||
|
provides: "Single listing application gate shared by API and worker"
|
||||||
|
contains: "reconcile_cloud_listing"
|
||||||
|
- path: "backend/tests/test_cloud_items.py"
|
||||||
|
provides: "Complete-versus-incomplete reconciliation and timestamp regression tests"
|
||||||
|
contains: "incomplete_listing"
|
||||||
|
- path: "backend/tests/test_cloud.py"
|
||||||
|
provides: "Connection-ID browse freshness integration tests"
|
||||||
|
contains: "complete_false"
|
||||||
|
key_links:
|
||||||
|
- from: "backend/api/cloud/browse.py"
|
||||||
|
to: "backend/services/cloud_items.py"
|
||||||
|
via: "shared listing application/finalization service"
|
||||||
|
pattern: "reconcile_cloud_listing"
|
||||||
|
- from: "backend/tasks/cloud_tasks.py"
|
||||||
|
to: "backend/services/cloud_items.py"
|
||||||
|
via: "the same service gate used by synchronous browse"
|
||||||
|
pattern: "reconcile_cloud_listing"
|
||||||
|
- from: "CloudListing.complete"
|
||||||
|
to: "CloudFolderState.refresh_state"
|
||||||
|
via: "fresh only for authoritative completion"
|
||||||
|
pattern: "complete.*fresh"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Make backend freshness truthful. Centralize how normalized listings are reconciled and finalized so incomplete, ambiguous, or failed provider results retain metadata and produce a controlled warning rather than a false fresh state.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||||
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@.planning/phases/12.1-fix-nextcloud-root-listing-and-sync-visibility/12.1-CONTEXT.md
|
||||||
|
@.planning/phases/12.1-fix-nextcloud-root-listing-and-sync-visibility/12.1-RESEARCH.md
|
||||||
|
@.planning/phases/12-cloud-resource-foundation/12-SECURITY.md
|
||||||
|
@backend/services/cloud_items.py
|
||||||
|
@backend/api/cloud/browse.py
|
||||||
|
@backend/tasks/cloud_tasks.py
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Specify complete, incomplete, and empty reconciliation semantics</name>
|
||||||
|
<files>backend/tests/test_cloud_items.py, backend/tests/test_cloud.py, backend/tests/test_cloud_security.py</files>
|
||||||
|
<read_first>
|
||||||
|
- backend/services/cloud_items.py — current reconciliation and update_folder_state behavior
|
||||||
|
- backend/api/cloud/browse.py — first-visit synchronous fresh transition
|
||||||
|
- backend/tasks/cloud_tasks.py — worker fresh transition and retry paths
|
||||||
|
- backend/tests/test_cloud_items.py — existing incomplete-retention tests
|
||||||
|
- backend/tests/test_cloud.py — browse response/freshness fixtures
|
||||||
|
</read_first>
|
||||||
|
<action>
|
||||||
|
Add red tests named `test_incomplete_listing_never_marks_folder_fresh`, `test_incomplete_listing_retains_cached_rows_and_last_success`, `test_complete_empty_listing_is_authoritative_and_fresh`, `test_partial_items_upsert_without_deleting_unseen_children`, `test_sync_browse_returns_warning_for_complete_false`, and `test_worker_returns_warning_for_complete_false`. Seed a previous successful timestamp and prove it is unchanged on incomplete/error results. Distinguish a provider-confirmed `CloudListing(items=(), complete=True)` from `complete=False`; only the former may soft-delete unseen children and update last_refreshed_at.
|
||||||
|
|
||||||
|
Add API/security assertions that responses remain whitelisted, warning messages are controlled, raw provider errors/URLs are absent, cached items remain owner scoped, and no get_object/download/MinIO/quota mutation occurs. Exercise both root `parent_ref=None` and a nested opaque provider reference.
|
||||||
|
</action>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- tests fail on the current unconditional fresh transitions
|
||||||
|
- tests cover service, synchronous API, and Celery worker paths
|
||||||
|
- complete-empty and incomplete-empty results have explicitly different outcomes
|
||||||
|
- cached rows and prior last_refreshed_at survive incomplete/error results
|
||||||
|
- owner/admin/credential/no-byte/no-quota assertions remain in the focused run
|
||||||
|
</acceptance_criteria>
|
||||||
|
<verify><automated>cd backend && pytest -q tests/test_cloud_items.py tests/test_cloud.py tests/test_cloud_security.py -k 'incomplete or complete_false or complete_empty or refresh or no_byte or quota'</automated></verify>
|
||||||
|
<done>Executable red tests define truthful completion semantics at every backend entry point.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Centralize listing application and freshness finalization</name>
|
||||||
|
<files>backend/services/cloud_items.py, backend/api/cloud/browse.py, backend/tasks/cloud_tasks.py, backend/api/cloud/schemas.py, backend/tests/test_cloud_items.py, backend/tests/test_cloud.py</files>
|
||||||
|
<read_first>
|
||||||
|
- backend/services/cloud_items.py — sole reconciliation entry point and service exception conventions
|
||||||
|
- backend/api/cloud/schemas.py — credential-free freshness response contract
|
||||||
|
- backend/api/cloud/browse.py — cached-first behavior and controlled warning copy
|
||||||
|
- backend/tasks/cloud_tasks.py — terminal/transient classification and retry behavior
|
||||||
|
- AGENTS.md — service layer must not raise HTTPException
|
||||||
|
</read_first>
|
||||||
|
<action>
|
||||||
|
Add one service-level operation in `services/cloud_items.py` that calls `reconcile_cloud_listing` and finalizes CloudFolderState according to listing completeness. Both browse.py and cloud_tasks.py must call it; neither caller may independently mark a listing fresh. Preserve `reconcile_cloud_listing` as the sole metadata reconciliation entry point. A complete listing sets fresh, clears controlled errors, and advances last_refreshed_at. An incomplete listing may upsert seen items but cannot delete unseen items, cannot advance last_refreshed_at, and sets warning with a stable code such as `incomplete_listing` and credential-free message.
|
||||||
|
|
||||||
|
Keep provider exceptions controlled: auth/credential failures remain terminal warnings; transient transport errors retain cached data and bounded retry behavior. Do not serialize raw exceptions, response bodies, full URLs, or credentials. Return a structured service result/domain exception—not HTTPException—so the API can still return cached metadata and authoritative FolderFreshnessOut. Ensure task return status differentiates complete success from incomplete warning without placing credentials or provider item names in broker/task results.
|
||||||
|
</action>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- no unconditional `refresh_state="fresh"` remains after adapter.list_folder in API or worker
|
||||||
|
- both paths use one service-level listing completion gate
|
||||||
|
- incomplete results expose controlled warning state and retain the previous successful timestamp
|
||||||
|
- complete empty results are accepted as fresh and reconcile correctly
|
||||||
|
- task arguments/results and API schemas contain no credentials or raw provider details
|
||||||
|
</acceptance_criteria>
|
||||||
|
<verify><automated>cd backend && pytest -q tests/test_cloud_items.py tests/test_cloud.py tests/test_cloud_security.py && ! rg -U 'listing = await adapter\.list_folder[\s\S]{0,800}refresh_state="fresh"' api/cloud/browse.py tasks/cloud_tasks.py</automated></verify>
|
||||||
|
<done>Synchronous and background refreshes share one authoritative completion gate and cannot report an incomplete listing as fresh.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 3: Document, security-review, commit, and push Plan 02</name>
|
||||||
|
<files>backend/main.py, frontend/package.json, frontend/package-lock.json, AGENTS.md, README.md, RUNBOOK.md, SECURITY.md, .planning/phases/12.1-fix-nextcloud-root-listing-and-sync-visibility/12.1-02-SUMMARY.md</files>
|
||||||
|
<action>Update required documentation for truthful freshness and the shared listing-application service. Bump the backend/frontend patch version once because synchronization status is user-visible, keeping package-lock metadata synchronized. Run a dedicated security agent over this plan's diff, resolve or escalate every finding, then run focused/full backend tests, Bandit, pip audit, Compose validation, and diff checks. Stage only Plan 02 files, exclude `.env` and unrelated work, commit atomically as `fix(12.1): make cloud freshness authoritative`, and push immediately.</action>
|
||||||
|
<verify><automated>docker compose config --quiet && cd backend && pytest -v && bandit -r . && pip audit && cd .. && git diff --check</automated></verify>
|
||||||
|
<done>Plan 02 has independent documentation, security approval, green gates, commit, and push.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<threat_model>
|
||||||
|
| Threat ID | Threat | Mitigation and evidence |
|
||||||
|
|-----------|--------|-------------------------|
|
||||||
|
| T-12.1-06 | False fresh state hides provider failure | Shared completion gate; complete=False warning tests in API and worker |
|
||||||
|
| T-12.1-07 | Incomplete listing deletes cached metadata | Reconcile deletion remains guarded by complete=True |
|
||||||
|
| T-12.1-08 | Error overwrites last known-good evidence | Prior timestamp preserved on incomplete/error refresh |
|
||||||
|
| T-12.1-09 | Provider errors leak secrets or URLs | Stable codes and controlled messages only |
|
||||||
|
| T-12.1-10 | Refresh changes bytes/quota | Existing and expanded no-byte/no-MinIO/no-quota assertions |
|
||||||
|
</threat_model>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
1. Demonstrate the new incomplete tests fail before implementation.
|
||||||
|
2. Run service tests for complete, complete-empty, partial, incomplete-empty, and exception paths.
|
||||||
|
3. Run connection-ID API and Celery task tests for the same state transitions.
|
||||||
|
4. Run owner/admin/credential/no-byte/no-quota security negatives.
|
||||||
|
5. Search proves callers no longer set fresh independently after list_folder.
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- Backend freshness means the provider supplied a complete authoritative listing.
|
||||||
|
- Cached metadata and last successful refresh evidence survive ambiguity/failure.
|
||||||
|
- Complete empty folders remain valid and reconcile correctly.
|
||||||
|
- Shared architecture and security boundaries remain intact.
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>Create `.planning/phases/12.1-fix-nextcloud-root-listing-and-sync-visibility/12.1-02-SUMMARY.md` when complete.</output>
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
---
|
||||||
|
phase: "12.1"
|
||||||
|
plan: "03"
|
||||||
|
type: tdd
|
||||||
|
wave: 3
|
||||||
|
depends_on:
|
||||||
|
- "12.1-02"
|
||||||
|
files_modified:
|
||||||
|
- frontend/src/views/CloudFolderView.vue
|
||||||
|
- frontend/src/stores/cloudConnections.js
|
||||||
|
- frontend/src/components/cloud/CloudProviderTreeItem.vue
|
||||||
|
- frontend/src/components/cloud/CloudFolderTreeItem.vue
|
||||||
|
- frontend/src/components/storage/StorageBrowser.vue
|
||||||
|
- frontend/src/views/__tests__/CloudFolderView.test.js
|
||||||
|
- frontend/src/stores/__tests__/cloudConnections.test.js
|
||||||
|
- frontend/src/components/cloud/__tests__/CloudProviderTreeItem.test.js
|
||||||
|
- frontend/src/components/cloud/__tests__/CloudFolderTreeItem.test.js
|
||||||
|
- frontend/src/components/storage/__tests__/StorageBrowser.skeleton.test.js
|
||||||
|
- frontend/src/components/cloud/__tests__/CloudBreadcrumbNavigation.test.js
|
||||||
|
- backend/main.py
|
||||||
|
- frontend/package.json
|
||||||
|
- frontend/package-lock.json
|
||||||
|
- AGENTS.md
|
||||||
|
- README.md
|
||||||
|
- RUNBOOK.md
|
||||||
|
- SECURITY.md
|
||||||
|
autonomous: true
|
||||||
|
requirements:
|
||||||
|
- CLOUD-01
|
||||||
|
- CONN-04
|
||||||
|
- SYNC-01
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "Cloud UI classifies items only with kind and never relies on the absent is_dir field"
|
||||||
|
- "Folder navigation sends provider_item_id while stable DocuVault id remains the row key/metadata identity"
|
||||||
|
- "The frontend renders response.freshness and never manufactures fresh state or a browser-clock refresh timestamp after HTTP 200"
|
||||||
|
- "Cached rows remain visible and navigable while freshness is refreshing or warning"
|
||||||
|
- "Opaque provider references, including slashes, question marks, hashes, percent signs, spaces, and Unicode, are transported without path interpretation"
|
||||||
|
- "Breadcrumbs come from explicit navigation lineage and never split provider_item_id into path segments"
|
||||||
|
- "CloudFolderView remains a thin data provider and StorageBrowser remains the single file browser"
|
||||||
|
artifacts:
|
||||||
|
- path: "frontend/src/views/CloudFolderView.vue"
|
||||||
|
provides: "Normalized item split, provider-reference navigation, and server freshness mapping"
|
||||||
|
contains: "provider_item_id"
|
||||||
|
- path: "frontend/src/views/__tests__/CloudFolderView.test.js"
|
||||||
|
provides: "kind/provider_item_id/truthful-freshness regressions"
|
||||||
|
contains: "warning"
|
||||||
|
- path: "frontend/src/components/cloud/__tests__/CloudFolderTreeItem.test.js"
|
||||||
|
provides: "Nested folder kind and provider reference tests"
|
||||||
|
contains: "provider_item_id"
|
||||||
|
key_links:
|
||||||
|
- from: "CloudBrowseResponse.items[].kind"
|
||||||
|
to: "StorageBrowser folders/files props"
|
||||||
|
via: "CloudFolderView computed classification"
|
||||||
|
pattern: "kind === 'folder'"
|
||||||
|
- from: "folder click/tree expansion"
|
||||||
|
to: "GET /api/cloud/connections/{id}/items?parent_ref="
|
||||||
|
via: "provider_item_id, never CloudItem.id"
|
||||||
|
pattern: "provider_item_id"
|
||||||
|
- from: "CloudBrowseResponse.freshness"
|
||||||
|
to: "StorageBrowser/BreadcrumbBar freshness props"
|
||||||
|
via: "Pinia browse state"
|
||||||
|
pattern: "refresh_state|last_refreshed_at"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Align the cloud frontend with the normalized API. Use `kind` for classification, `provider_item_id` for provider navigation, stable `id` for DocuVault row identity, and backend freshness as the only source of synchronization truth.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||||
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@.planning/phases/12.1-fix-nextcloud-root-listing-and-sync-visibility/12.1-CONTEXT.md
|
||||||
|
@.planning/phases/12.1-fix-nextcloud-root-listing-and-sync-visibility/12.1-PATTERNS.md
|
||||||
|
@.planning/phases/12-cloud-resource-foundation/12-03-PLAN.md
|
||||||
|
@frontend/src/views/CloudFolderView.vue
|
||||||
|
@frontend/src/stores/cloudConnections.js
|
||||||
|
@frontend/src/components/storage/StorageBrowser.vue
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Add red normalized-item and navigation regressions</name>
|
||||||
|
<files>frontend/src/views/__tests__/CloudFolderView.test.js, frontend/src/components/cloud/__tests__/CloudProviderTreeItem.test.js, frontend/src/components/cloud/__tests__/CloudFolderTreeItem.test.js, frontend/src/components/storage/__tests__/StorageBrowser.skeleton.test.js, frontend/src/components/cloud/__tests__/CloudBreadcrumbNavigation.test.js</files>
|
||||||
|
<read_first>
|
||||||
|
- frontend/src/views/CloudFolderView.vue — current is_dir filtering, item.id navigation, and fabricated freshness
|
||||||
|
- frontend/src/components/cloud/CloudProviderTreeItem.vue — root folder filtering
|
||||||
|
- frontend/src/components/cloud/CloudFolderTreeItem.vue — nested filtering and navigation
|
||||||
|
- frontend/src/components/storage/StorageBrowser.vue — shared row keys/events and freshness props
|
||||||
|
- frontend/src/components/cloud/__tests__/CloudBreadcrumbNavigation.test.js — explicit lineage and opaque-reference breadcrumb regressions
|
||||||
|
- backend/api/cloud/schemas.py — canonical response shape
|
||||||
|
</read_first>
|
||||||
|
<action>
|
||||||
|
Replace legacy-shaped fixtures with realistic CloudItemOut objects containing distinct `id` and `provider_item_id`, `kind`, nullable metadata, and no `is_dir`. Add tests named `renders_kind_folder_and_file_in_shared_browser`, `folder_click_uses_provider_item_id_not_id`, `tree_expansion_filters_kind_folder`, `nested_tree_uses_provider_item_id`, and `stable_docuvault_id_remains_row_key`. Include duplicate display names with distinct IDs/references so tests cannot pass accidentally by name.
|
||||||
|
|
||||||
|
Add opaque-reference fixtures containing `/`, `?`, `#`, `%`, spaces, and Unicode. Assert CloudFolderView only passes props/emits events through StorageBrowser and does not add another grid/tree implementation. Verify root sentinel remains `parent_ref=null` at the API boundary and nested folders pass their opaque provider reference unchanged through named Vue Router navigation and the API client's query serializer—never manual route-string interpolation.
|
||||||
|
|
||||||
|
Add breadcrumb tests proving labels and navigation refs come from an explicit session-only lineage of visited `{name, provider_item_id}` folder nodes (or equivalent API-provided ancestors), not `split('/')` or any other parsing of provider references. Breadcrumb clicks must restore the exact stored opaque reference.
|
||||||
|
</action>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- tests fail against current is_dir and item.id behavior
|
||||||
|
- fixtures omit is_dir and distinguish id from provider_item_id
|
||||||
|
- root and nested provider navigation are both covered
|
||||||
|
- StorageBrowser remains the rendered browser component
|
||||||
|
- no test relies on provider-specific path reconstruction in Vue
|
||||||
|
- reserved-character provider references survive route/query transport and breadcrumb navigation unchanged
|
||||||
|
</acceptance_criteria>
|
||||||
|
<verify><automated>cd frontend && npm test -- --run src/views/__tests__/CloudFolderView.test.js src/components/cloud/__tests__/CloudProviderTreeItem.test.js src/components/cloud/__tests__/CloudFolderTreeItem.test.js src/components/storage/__tests__/StorageBrowser.skeleton.test.js src/components/cloud/__tests__/CloudBreadcrumbNavigation.test.js</automated></verify>
|
||||||
|
<done>Red frontend tests reproduce invisible folders and wrong-reference navigation using the actual API shape.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Make backend freshness authoritative in Pinia and the view</name>
|
||||||
|
<files>frontend/src/stores/cloudConnections.js, frontend/src/views/CloudFolderView.vue, frontend/src/stores/__tests__/cloudConnections.test.js, frontend/src/views/__tests__/CloudFolderView.test.js</files>
|
||||||
|
<read_first>
|
||||||
|
- frontend/src/stores/cloudConnections.js — folderFreshness and lastRefreshedAt state API
|
||||||
|
- frontend/src/views/CloudFolderView.vue — unconditional fresh/browser-clock assignment
|
||||||
|
- frontend/src/components/ui/BreadcrumbBar.vue — expected refreshing/fresh/warning values and timestamp rendering
|
||||||
|
- frontend/src/views/__tests__/CloudFolderView.test.js — existing fetch/error behavior
|
||||||
|
- frontend/src/stores/__tests__/cloudConnections.test.js — store reset/persistence conventions
|
||||||
|
</read_first>
|
||||||
|
<action>
|
||||||
|
Add tests `maps_server_refreshing_freshness`, `maps_server_warning_and_last_success`, `http_200_does_not_imply_fresh`, `does_not_use_browser_clock_as_refresh_evidence`, and `cached_items_remain_visible_during_warning`. Map `response.freshness.refresh_state`, `last_refreshed_at`, `error_code`, and controlled `error_message` into Pinia. Keep a request-in-flight UI state only if it cannot overwrite the last authoritative server state; after the response, render the server object verbatim. A successful cached browse response is not proof of provider synchronization.
|
||||||
|
|
||||||
|
Remove `new Date().toISOString()` and any unconditional `fresh` assignment from CloudFolderView. On request failure without a server freshness payload, preserve cached items and prior last successful timestamp while showing a controlled client transport warning; never invent a provider refresh time.
|
||||||
|
</action>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- HTTP 200 plus warning freshness renders warning, not fresh
|
||||||
|
- server last_refreshed_at is preserved exactly and browser Date is not called to create evidence
|
||||||
|
- error code/message remain controlled display data and do not affect item ownership/navigation
|
||||||
|
- cached items remain usable during refreshing/warning states
|
||||||
|
- store reset clears cloud browse state without persisting secrets
|
||||||
|
</acceptance_criteria>
|
||||||
|
<verify><automated>cd frontend && npm test -- --run src/stores/__tests__/cloudConnections.test.js src/views/__tests__/CloudFolderView.test.js</automated></verify>
|
||||||
|
<done>The UI reports backend freshness truthfully and preserves cached navigation through warnings.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 3: Implement normalized item use across cloud view and trees</name>
|
||||||
|
<files>frontend/src/views/CloudFolderView.vue, frontend/src/components/cloud/CloudProviderTreeItem.vue, frontend/src/components/cloud/CloudFolderTreeItem.vue, frontend/src/components/storage/StorageBrowser.vue, frontend/src/views/__tests__/CloudFolderView.test.js, frontend/src/components/cloud/__tests__/CloudProviderTreeItem.test.js, frontend/src/components/cloud/__tests__/CloudFolderTreeItem.test.js, frontend/src/components/cloud/__tests__/CloudBreadcrumbNavigation.test.js</files>
|
||||||
|
<read_first>
|
||||||
|
- frontend/src/components/ui/TreeItem.vue — shared expand/collapse state contract
|
||||||
|
- frontend/src/components/storage/StorageBrowser.vue — single browser row/event contract
|
||||||
|
- frontend/src/views/CloudFolderView.vue — thin view boundary
|
||||||
|
- frontend/src/components/cloud/CloudProviderTreeItem.vue — root folder source
|
||||||
|
- frontend/src/components/cloud/CloudFolderTreeItem.vue — nested folder source
|
||||||
|
</read_first>
|
||||||
|
<action>
|
||||||
|
Replace all cloud `is_dir` checks with `item.kind === "folder"` and file checks with `item.kind === "file"`. Route and browse nested folders with `provider_item_id`; retain `id` for Vue keys, selection, and DocuVault metadata identity. Do not add an alias field, provider-specific mapper, alternate browser, or duplicate expand/collapse state. Keep CloudProviderTreeItem and CloudFolderTreeItem wrapped by TreeItem.vue.
|
||||||
|
|
||||||
|
Use named-route objects for connection routing and keep `parent_ref` in serialized query/API state; do not interpolate it into a route path. Ensure folder references are encoded only by Vue Router/API query serializers, not manually concatenated or decoded. Maintain explicit session-only breadcrumb lineage from root to visited folders; never reconstruct hierarchy by splitting `provider_item_id`, because Drive/OneDrive IDs and DAV references are opaque. Preserve connection UUID routing from Phase 12. Remove dead compatibility logic in the same change.
|
||||||
|
</action>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- no active cloud component references item.is_dir
|
||||||
|
- every provider browse/navigation call uses provider_item_id for nested parent_ref
|
||||||
|
- stable id remains the rendering/metadata identity and is never sent to a provider adapter
|
||||||
|
- CloudFolderView contains no grid/layout duplication
|
||||||
|
- focused and full cloud frontend suites pass
|
||||||
|
- opaque IDs containing reserved characters and breadcrumb back-navigation round-trip exactly
|
||||||
|
</acceptance_criteria>
|
||||||
|
<verify><automated>cd frontend && npm test -- --run src/views/__tests__/CloudFolderView.test.js src/components/cloud/__tests__/CloudProviderTreeItem.test.js src/components/cloud/__tests__/CloudFolderTreeItem.test.js src/components/storage/__tests__/StorageBrowser.skeleton.test.js src/stores/__tests__/cloudConnections.test.js src/components/cloud/__tests__/CloudBreadcrumbNavigation.test.js && ! rg '\.is_dir' src/views/CloudFolderView.vue src/components/cloud</automated></verify>
|
||||||
|
<done>Cloud folders render and navigate with the canonical API fields while shared component boundaries remain intact.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 4: Document, security-review, commit, and push Plan 03</name>
|
||||||
|
<files>AGENTS.md, README.md, RUNBOOK.md, SECURITY.md, .planning/phases/12.1-fix-nextcloud-root-listing-and-sync-visibility/12.1-03-SUMMARY.md</files>
|
||||||
|
<action>Update required docs for normalized cloud UI routing, breadcrumb lineage, and server freshness. Bump the backend/frontend patch version once and synchronize package-lock metadata because the plan changes user-visible behavior. Run a dedicated security agent against this plan's diff, resolving or escalating findings. Run focused/full frontend tests, production build, npm audit, Compose validation, and diff checks. Stage only Plan 03 files, exclude `.env` and unrelated work, commit atomically as `fix(12.1): align cloud browser contract`, and push immediately.</action>
|
||||||
|
<verify><automated>docker compose config --quiet && cd frontend && npm test && npm run build && npm audit --audit-level=high && cd .. && git diff --check</automated></verify>
|
||||||
|
<done>Plan 03 has independent documentation, security approval, green gates, commit, and push.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<threat_model>
|
||||||
|
| Threat ID | Threat | Mitigation and evidence |
|
||||||
|
|-----------|--------|-------------------------|
|
||||||
|
| T-12.1-11 | DocuVault UUID sent as provider path | Distinct-ID fixtures and provider_item_id navigation assertions |
|
||||||
|
| T-12.1-12 | UI falsely reports provider synchronization | Server freshness mapping; no browser-clock evidence |
|
||||||
|
| T-12.1-13 | Provider reference injected into provider-specific URL | API query serialization; adapters own provider translation/validation |
|
||||||
|
| T-12.1-14 | Secrets persisted in frontend state | Existing memory/session rules; tests store folder reference only |
|
||||||
|
| T-12.1-15 | Parallel browser/tree logic drifts | StorageBrowser and TreeItem remain canonical shared components |
|
||||||
|
</threat_model>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
1. Demonstrate actual API-shaped fixtures fail current is_dir/item.id code.
|
||||||
|
2. Verify kind-based root and nested classification.
|
||||||
|
3. Verify provider_item_id navigation while id remains row identity.
|
||||||
|
4. Verify warning/refreshing/fresh server payloads and exact timestamps.
|
||||||
|
5. Search for legacy is_dir usage in active cloud components and run the focused frontend suite.
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- Users can see folders/files returned by every normalized provider adapter.
|
||||||
|
- Folder clicks and tree expansion use valid provider references.
|
||||||
|
- Synchronization wording reflects backend state rather than HTTP status or client time.
|
||||||
|
- Shared StorageBrowser/TreeItem architecture remains unchanged.
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>Create `.planning/phases/12.1-fix-nextcloud-root-listing-and-sync-visibility/12.1-03-SUMMARY.md` when complete.</output>
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
---
|
||||||
|
phase: "12.1"
|
||||||
|
plan: "04"
|
||||||
|
type: execute
|
||||||
|
wave: 4
|
||||||
|
depends_on:
|
||||||
|
- "12.1-01"
|
||||||
|
- "12.1-02"
|
||||||
|
- "12.1-03"
|
||||||
|
files_modified:
|
||||||
|
- backend/tests/test_nextcloud_live.py
|
||||||
|
- backend/tests/fixtures/cloud/nextcloud_expected_root.json
|
||||||
|
- backend/pytest.ini
|
||||||
|
- frontend/src/views/__tests__/CloudFolderRenderedFlow.test.js
|
||||||
|
- .planning/phases/12.1-fix-nextcloud-root-listing-and-sync-visibility/12.1-VALIDATION.md
|
||||||
|
- backend/main.py
|
||||||
|
- frontend/package.json
|
||||||
|
- frontend/package-lock.json
|
||||||
|
- AGENTS.md
|
||||||
|
- README.md
|
||||||
|
- RUNBOOK.md
|
||||||
|
- SECURITY.md
|
||||||
|
autonomous: false
|
||||||
|
requirements:
|
||||||
|
- CONN-04
|
||||||
|
- CLOUD-01
|
||||||
|
- CACHE-01
|
||||||
|
- SYNC-01
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "An opt-in read-only live test browses the configured Nextcloud root through the owner-scoped connection-ID DocuVault API as testuser@docuvault.example"
|
||||||
|
- "Live execution never prints secrets, the full URL, or unexpected provider names and never downloads/mutates provider data"
|
||||||
|
- "Live validation has a nonblocking sanitized diagnostic mode and a separate exact acceptance mode enabled only by an owner-confirmed tracked manifest"
|
||||||
|
- "Mocked and live evidence together prove folders/files, kind, provider identity, freshness, zero-byte, and owner isolation"
|
||||||
|
- "Full tests, security/dependency/secret gates, docs, version, atomic commit, and push complete before Phase 12.1 closes"
|
||||||
|
artifacts:
|
||||||
|
- path: "backend/tests/test_nextcloud_live.py"
|
||||||
|
provides: "Opt-in sanitized live Nextcloud adapter and connection-ID API smoke"
|
||||||
|
contains: "test_nextcloud_root_diagnostic"
|
||||||
|
- path: "backend/tests/fixtures/cloud/nextcloud_expected_root.json"
|
||||||
|
provides: "Owner-confirmed, non-secret exact root name/kind acceptance manifest"
|
||||||
|
contains: "owner_confirmed"
|
||||||
|
- path: ".planning/phases/12.1-fix-nextcloud-root-listing-and-sync-visibility/12.1-VALIDATION.md"
|
||||||
|
provides: "Nyquist matrix and live/mock/security evidence"
|
||||||
|
contains: "7 of 10"
|
||||||
|
- path: "SECURITY.md"
|
||||||
|
provides: "Phase 12.1 threat and gate evidence without secrets/provider content"
|
||||||
|
contains: "Phase 12.1"
|
||||||
|
key_links:
|
||||||
|
- from: "NEXTCLOUD_URL/NEXTCLOUD_USER/NEXTCLOUD_APP_PASSWORD"
|
||||||
|
to: "backend/tests/test_nextcloud_live.py"
|
||||||
|
via: "ignored environment read in process memory only"
|
||||||
|
pattern: 'os\.environ|getenv'
|
||||||
|
- from: "testuser@docuvault.example"
|
||||||
|
to: "GET /api/cloud/connections/{connection_id}/items"
|
||||||
|
via: "isolated test DB user, encrypted connection, authenticated API request"
|
||||||
|
pattern: 'testuser@docuvault\.example'
|
||||||
|
- from: "supplied expected manifest"
|
||||||
|
to: "exact live signoff"
|
||||||
|
via: "explicit discrepancy reconciliation gate"
|
||||||
|
pattern: 'Documents|Reasons to use Nextcloud\.pdf'
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Prove the repaired flow against the designated test account without risking credentials or provider data, reconcile the known manifest discrepancy before exact-name acceptance, then close Phase 12.1 with full validation, security, documentation, versioning, commit, and push protocol.
|
||||||
|
|
||||||
|
The live test is intentionally opt-in and read-only. Missing credentials skip with a prerequisite message; a manifest discrepancy blocks exact-name signoff rather than mutating the provider or silently changing expectations.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||||
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@.planning/phases/12.1-fix-nextcloud-root-listing-and-sync-visibility/12.1-CONTEXT.md
|
||||||
|
@.planning/phases/12.1-fix-nextcloud-root-listing-and-sync-visibility/12.1-RESEARCH.md
|
||||||
|
@.planning/phases/12.1-fix-nextcloud-root-listing-and-sync-visibility/12.1-PATTERNS.md
|
||||||
|
@.planning/phases/12-cloud-resource-foundation/12-VALIDATION.md
|
||||||
|
@.planning/phases/12-cloud-resource-foundation/12-SECURITY.md
|
||||||
|
@.planning/phases/12-cloud-resource-foundation/12-VERIFICATION.md
|
||||||
|
@AGENTS.md
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Build and run the nonblocking sanitized live diagnostic</name>
|
||||||
|
<files>backend/tests/test_nextcloud_live.py, backend/pytest.ini, .planning/phases/12.1-fix-nextcloud-root-listing-and-sync-visibility/12.1-VALIDATION.md</files>
|
||||||
|
<read_first>
|
||||||
|
- .planning/phases/12.1-fix-nextcloud-root-listing-and-sync-visibility/12.1-CONTEXT.md — credentials, user, expected manifest, and read-only rules
|
||||||
|
- .planning/phases/12.1-fix-nextcloud-root-listing-and-sync-visibility/12.1-RESEARCH.md — sanitized 207/10/7-of-10 probe evidence
|
||||||
|
- backend/tests/conftest.py — isolated DB/auth fixture conventions
|
||||||
|
- backend/tests/test_cloud.py — connection creation and connection-ID browse patterns
|
||||||
|
- backend/tests/test_cloud_security.py — owner/admin/credential/no-byte assertions
|
||||||
|
- backend/storage/cloud_utils.py — credential encryption and URL validation
|
||||||
|
</read_first>
|
||||||
|
<action>
|
||||||
|
Add a `live_nextcloud` pytest marker excluded from ordinary test runs unless explicitly selected. Read only `NEXTCLOUD_URL`, `NEXTCLOUD_USER`, and `NEXTCLOUD_APP_PASSWORD` from the ignored environment at runtime; if any are absent, skip with a generic prerequisite message naming variable keys only. Never load or print `.env`, and never place values in parametrization IDs, assertion diffs, exceptions, snapshots, logs, artifacts, or command lines.
|
||||||
|
|
||||||
|
Create `test_nextcloud_adapter_read_only_root_metadata`, `test_nextcloud_root_diagnostic`, and an authenticated `test_nextcloud_connection_id_browse_as_designated_user`. In an isolated test database, provision/resolve the regular DocuVault user exactly `testuser@docuvault.example`, encrypt the live connection credentials through production helpers, then browse root through `GET /api/cloud/connections/{connection_id}/items`; also assert foreign-user/admin denial. The test may issue metadata-only DAV requests required for listing. Install a request guard that rejects byte GET/media and PUT/POST/PATCH/MKCOL/MOVE/COPY/DELETE methods before network dispatch; assert quota and MinIO/object storage are unchanged.
|
||||||
|
|
||||||
|
In diagnostic mode compare in memory against the ten owner-supplied candidate names, but do not assert exact equality and do not claim acceptance. Emit only total count, expected-match count, missing supplied names, unexpected count, completion state, and kind counts; never emit unexpected names. The diagnostic must exit successfully when transport/security/read-only invariants pass even if the candidate manifest is 7/10, while recording `manifest_status: unconfirmed`. Catch/redact transport errors into stable codes so neither full URL nor credentials enter pytest output. Assert captured logs/output contain none of the secret values or full URL.
|
||||||
|
|
||||||
|
Record the contract, commands, skip behavior, sanitized result fields, and threat references in 12.1-VALIDATION.md. Do not record unexpected live names.
|
||||||
|
</action>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- ordinary pytest does not contact Nextcloud and missing variables produce an explicit safe skip
|
||||||
|
- selected live tests authenticate only as the designated regular app user and use connection-ID browse
|
||||||
|
- network guard permits metadata listing but rejects download and every mutation method
|
||||||
|
- output contains no credential, full URL, raw response body, or unexpected provider name
|
||||||
|
- manifest mismatch is diagnostic-only and nonblocking before owner confirmation
|
||||||
|
</acceptance_criteria>
|
||||||
|
<verify><automated>cd backend && pytest -q tests/test_nextcloud_live.py -m live_nextcloud</automated></verify>
|
||||||
|
<done>The opt-in diagnostic safely proves connectivity/listing invariants and reports sanitized drift without claiming exact acceptance.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="checkpoint:human-verify" gate="blocking">
|
||||||
|
<name>Task 2: Reconcile the known 7-of-10 fixture discrepancy before exact signoff</name>
|
||||||
|
<files>backend/tests/fixtures/cloud/nextcloud_expected_root.json, backend/tests/test_nextcloud_live.py, .planning/phases/12.1-fix-nextcloud-root-listing-and-sync-visibility/12.1-VALIDATION.md</files>
|
||||||
|
<read_first>
|
||||||
|
- .planning/phases/12.1-fix-nextcloud-root-listing-and-sync-visibility/12.1-RESEARCH.md — three supplied names not exactly matched in sanitized probe
|
||||||
|
- backend/tests/test_nextcloud_live.py — supplied expected manifest and safe mismatch report
|
||||||
|
</read_first>
|
||||||
|
<action>
|
||||||
|
Run the live smoke and inspect only its sanitized count/missing-expected output. The prior probe found 10 children but only 7 exact supplied-name matches; therefore do not declare the ten-name assertion accepted merely because item count is 10. If the same mismatch remains, stop and ask the project owner to compare the three supplied expected names (`Manual Nextcloud.png`, `Nextcloud Intro.mp4`, `Template credits.md`) with the Nextcloud UI and state which source is authoritative. Do not reveal the three unexpected provider names and do not rename/delete/upload anything.
|
||||||
|
|
||||||
|
After explicit owner confirmation, store the authoritative names and kinds in tracked `backend/tests/fixtures/cloud/nextcloud_expected_root.json` with no URL, username, credential, provider ID, timestamp, or unexpected-name discovery data; include `owner_confirmed: true` and a short non-secret provenance note. Only then add/enable `test_nextcloud_expected_root_manifest`, loading that fixture. If the original supplied expectation is authoritative, leave its names unchanged and treat the provider account as requiring external fixture correction. Re-run until exact set equality and exact kind equality pass. Record the owner-confirmed disposition and only sanitized counts in 12.1-VALIDATION.md.
|
||||||
|
</action>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- exact-name signoff is blocked while only 7/10 supplied names match
|
||||||
|
- no unexpected provider names are printed or persisted during reconciliation
|
||||||
|
- any expected-manifest change has explicit owner confirmation, not inference from live output
|
||||||
|
- final live result is exact set and kind equality, not count-only acceptance
|
||||||
|
- provider content remains unchanged
|
||||||
|
</acceptance_criteria>
|
||||||
|
<verify><automated>cd backend && pytest -q tests/test_nextcloud_live.py -m live_nextcloud -k expected_root_manifest</automated></verify>
|
||||||
|
<done>The owner has reconciled the fixture discrepancy and the live account passes exact root-name and kind acceptance without provider mutation.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 3: Run full regression, security, environment, and rendered-flow gates</name>
|
||||||
|
<files>frontend/src/views/__tests__/CloudFolderRenderedFlow.test.js, .planning/phases/12.1-fix-nextcloud-root-listing-and-sync-visibility/12.1-VALIDATION.md, SECURITY.md, RUNBOOK.md</files>
|
||||||
|
<read_first>
|
||||||
|
- AGENTS.md — mandatory tests, security checks, environment contract, and deployment exclusions
|
||||||
|
- .planning/phases/12-cloud-resource-foundation/12-VALIDATION.md — baseline Nyquist matrix and commands
|
||||||
|
- .planning/phases/12-cloud-resource-foundation/12-SECURITY.md — inherited threat evidence
|
||||||
|
- SECURITY.md — project security evidence format
|
||||||
|
- RUNBOOK.md — operational cloud/migration guidance
|
||||||
|
</read_first>
|
||||||
|
<action>
|
||||||
|
Add an executable Vue Test Utils rendered-flow test `CloudFolderRenderedFlow.test.js` that mounts the real CloudFolderView with StorageBrowser/BreadcrumbBar (stubbing only network/router boundaries), renders mixed root folders/files, clicks a folder whose opaque reference contains reserved characters, navigates back via lineage breadcrumbs, and renders refreshing/warning/fresh server states while cached rows remain visible. Run it in the normal Vitest command. Use the live account only for the separate sanitized API smoke; do not place provider names or credentials in rendered snapshots.
|
||||||
|
|
||||||
|
Run `bandit -r backend/`, `pip audit`, `npm audit --audit-level=high`, security-invariant tests, and `gitleaks detect --source . --no-banner --redact` as the concrete repository/history secret scanner (install the pinned approved binary before execution if absent; absence blocks the gate). Also run `git ls-files '.env' '.env.*'` and require only approved example files. Verify no new high/critical findings, no tracked `.env`, no secret values in current history/diff, no raw provider URL/error leakage, no admin/foreign-user access, and no provider byte/mutation/quota activity. Resolve introduced findings without skips, suppressions, or broad exception swallowing. Update validation/security/runbook evidence with commands and sanitized outcomes only.
|
||||||
|
</action>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- four-provider focused contract, live exact acceptance, full suites, and build pass
|
||||||
|
- Compose config resolves every required variable non-empty without printing values
|
||||||
|
- Bandit and pip/npm audits have zero introduced high/critical findings
|
||||||
|
- secret scan and git diff contain no credentials/full live URL
|
||||||
|
- rendered root/nested/freshness behavior matches API truth and uses StorageBrowser
|
||||||
|
</acceptance_criteria>
|
||||||
|
<verify><automated>docker compose config --quiet && gitleaks detect --source . --no-banner --redact && test -z "$(git ls-files '.env' '.env.*' | grep -v '^\.env\.example$')" && cd backend && pytest -q tests/test_cloud_provider_contract.py tests/test_cloud_backends.py tests/test_webdav_backend.py tests/test_cloud_items.py tests/test_cloud.py tests/test_cloud_security.py && pytest -q tests/test_nextcloud_live.py -m live_nextcloud && pytest -v && bandit -r . && pip audit && cd ../frontend && npm test -- --run src/views/__tests__/CloudFolderRenderedFlow.test.js && npm test && npm run build && npm audit --audit-level=high</automated></verify>
|
||||||
|
<done>Automated, live, rendered, security, dependency, secret, and environment gates pass with sanitized evidence.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 4: Close documentation, version, commit, and push protocol</name>
|
||||||
|
<files>backend/main.py, frontend/package.json, frontend/package-lock.json, AGENTS.md, README.md, RUNBOOK.md, SECURITY.md, .planning/phases/12.1-fix-nextcloud-root-listing-and-sync-visibility/12.1-VALIDATION.md</files>
|
||||||
|
<read_first>
|
||||||
|
- backend/main.py — backend version source of truth
|
||||||
|
- frontend/package.json — frontend version source of truth
|
||||||
|
- frontend/package-lock.json — lockfile root/package version entries
|
||||||
|
- AGENTS.md — current state, shared module map, documentation/version/Git protocols
|
||||||
|
- README.md — cloud browse feature and environment documentation
|
||||||
|
- .planning/ROADMAP.md — Phase 12.1 boundary and Phase 13/14 exclusions
|
||||||
|
</read_first>
|
||||||
|
<action>
|
||||||
|
Update AGENTS.md current state to Phase 12.1 complete and add any new canonical listing/freshness helper to the backend shared-module map with a no-duplication rule. Update README with truthful provider-neutral browse behavior and opt-in live test variable names only; never add values. Update RUNBOOK with safe live-smoke invocation, redaction expectations, warning semantics, and fixture-reconciliation procedure. Update SECURITY.md and 12.1-VALIDATION.md with final evidence and inherited controls.
|
||||||
|
|
||||||
|
Increment the current backend/frontend patch version exactly once for Plan 04 in backend/main.py, frontend/package.json, and both relevant package-lock entries; earlier plans have already performed their own required per-plan bumps. Use source values at execution time and keep all versions identical. Do not claim Phase 13 mutations or Phase 14 byte cache behavior.
|
||||||
|
|
||||||
|
Run a dedicated security agent on Plan 04's diff and resolve or escalate every finding. Re-run focused/full tests, rendered test, gitleaks, build, security gates, and `docker compose config --quiet` after docs/version changes. Review `git diff --check`, `git status`, and staged diff for secrets/unrelated user changes. Stage only Plan 04 live-test/fixture/rendered-test/planning/docs/version files explicitly, commit atomically as `test(12.1): verify live cloud browse integration`, and push immediately. Never amend or accumulate Plans 01-03, stage `.env`, or overwrite unrelated work.
|
||||||
|
</action>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- AGENTS/README/RUNBOOK/SECURITY describe shipped behavior and explicit Phase 13/14 exclusions
|
||||||
|
- documentation names environment keys only and contains no credential, full URL, or unexpected live provider name
|
||||||
|
- backend/package/lock versions match after one patch bump
|
||||||
|
- final test/build/security/config gates remain green
|
||||||
|
- Plan 04 has its own atomic commit and push without unrelated files or `.env`; Plans 01-03 remain separate commits
|
||||||
|
</acceptance_criteria>
|
||||||
|
<verify><automated>docker compose config --quiet && git diff --check && cd backend && pytest -v && cd ../frontend && npm test && npm run build && npm audit --audit-level=high</automated></verify>
|
||||||
|
<done>Phase 12.1 is documented, versioned, fully verified, atomically committed, and pushed with no secret or scope leakage.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<threat_model>
|
||||||
|
| Threat ID | Threat | Mitigation and evidence |
|
||||||
|
|-----------|--------|-------------------------|
|
||||||
|
| T-12.1-16 | Live credentials/full URL leak through pytest/logs | Runtime env only, generic failures, capture assertions, no values in IDs/commands/artifacts |
|
||||||
|
| T-12.1-17 | Live smoke mutates/downloads user data | Network method guard; metadata-only requests; no-byte/no-quota assertions |
|
||||||
|
| T-12.1-18 | Live connection crosses user/admin boundary | Designated regular user plus foreign/admin negatives through connection-ID API |
|
||||||
|
| T-12.1-19 | Count-only test accepts wrong root | Exact set and kind equality; blocking 7/10 reconciliation checkpoint |
|
||||||
|
| T-12.1-20 | Unexpected provider names become persisted sensitive metadata | Unexpected count only; no names in output/docs/fixtures |
|
||||||
|
| T-12.1-SC | Supply-chain or committed-secret regression | Bandit, pip/npm audits, secret scan, staged diff review before commit/push |
|
||||||
|
</threat_model>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
1. Ordinary tests skip live access safely; selected live tests use only the three named environment keys.
|
||||||
|
2. Live adapter and connection-ID API smoke pass as the designated user with exact names/kinds after explicit discrepancy reconciliation.
|
||||||
|
3. Network guard and DB assertions prove no provider mutation, download, MinIO, or quota change.
|
||||||
|
4. Shared four-provider, backend/frontend full, build, security, audit, secret, and Compose gates pass.
|
||||||
|
5. Docs/version are synchronized; atomic commit and push succeed without `.env` or unrelated changes.
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- The connected Nextcloud root is visible through DocuVault and matches an owner-confirmed exact manifest.
|
||||||
|
- The test design validates all providers through one contract while preserving provider-specific fixtures and pagination.
|
||||||
|
- Freshness is truthful from adapter completion through API, store, and rendered browser.
|
||||||
|
- Credentials/provider data remain private and provider content remains untouched.
|
||||||
|
- Phase 12.1 closes without importing Phase 13 mutations or Phase 14 byte caching.
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>Create `.planning/phases/12.1-fix-nextcloud-root-listing-and-sync-visibility/12.1-04-SUMMARY.md` when complete, then update roadmap/state through the normal GSD phase-close workflow.</output>
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# Phase 12.1: Fix Nextcloud Root Listing and Sync Visibility - Context
|
||||||
|
|
||||||
|
**Gathered:** 2026-06-22
|
||||||
|
**Status:** Ready for planning
|
||||||
|
**Source:** User-reported integration failure and test-account manifest
|
||||||
|
|
||||||
|
<domain>
|
||||||
|
## Phase Boundary
|
||||||
|
|
||||||
|
Diagnose and fix the cloud browse path that can report a successful/fresh synchronization while showing no provider files or folders. Prove the repair against the configured Nextcloud test account and add provider-neutral listing contracts for Nextcloud, generic WebDAV, Google Drive, and OneDrive. This phase repairs browse/list correctness and truthful freshness; cloud mutations remain Phase 13 scope.
|
||||||
|
|
||||||
|
</domain>
|
||||||
|
|
||||||
|
<decisions>
|
||||||
|
## Implementation Decisions
|
||||||
|
|
||||||
|
### Live Nextcloud acceptance contract
|
||||||
|
- D-01: The ignored repository `.env` provides the opt-in live-test variables `NEXTCLOUD_URL`, `NEXTCLOUD_USER`, and `NEXTCLOUD_APP_PASSWORD`; credentials must never be printed, logged, committed, placed in test IDs, or copied into planning artifacts.
|
||||||
|
- D-02: The DocuVault application identity used for the live integration flow is `testuser@docuvault.example`.
|
||||||
|
- D-03: A successful root browse must expose exactly these expected entry names, with folder/file kind checked separately: `Documents`, `docuvault`, `Photos`, `Templates`, `Nextcloud.png`, `Nextcloud Intro.mp4`, `Manual Nextcloud.png`, `Readme.md`, `Reasons to use Nextcloud.pdf`, `Template credits.md`.
|
||||||
|
- D-04: Live provider tests are opt-in, read-only, and skip with an explicit prerequisite message when credentials are absent. They may list metadata but must not upload, create, rename, move, delete, download bytes, or change quota.
|
||||||
|
|
||||||
|
### Provider-wide correctness
|
||||||
|
- D-05: The fix must establish one provider-neutral listing correctness contract for Nextcloud, generic WebDAV, Google Drive, and OneDrive rather than adding a Nextcloud-only API or UI path.
|
||||||
|
- D-06: Every adapter contract test must prove normalized child identity, parent reference, name, file/folder kind, metadata completeness, pagination/completeness semantics, and zero byte-download/mutation calls.
|
||||||
|
- D-07: A refresh may transition to `fresh` only after a valid listing has been normalized and reconciled. Empty listings are valid only when the provider positively returned an empty collection; parser/transport/root-resolution ambiguity must become a controlled warning/error, not “synced”.
|
||||||
|
- D-08: Cached metadata remains visible during refresh and after provider failure. A failed or ambiguous refresh must not delete cached children or claim a successful empty root.
|
||||||
|
|
||||||
|
### Security and architecture
|
||||||
|
- D-09: Preserve owner and connection scoping at every API/service boundary; the live connection belongs only to the designated regular test user and admin access remains blocked.
|
||||||
|
- D-10: Provider credentials are decrypted only at the provider/task boundary and never enter broker payloads, API responses, browser storage, logs, snapshots, fixtures, or committed files.
|
||||||
|
- D-11: All outbound WebDAV/Nextcloud requests continue through canonical SSRF validation; redirects and resolved addresses must not bypass the allowlist/private-address controls.
|
||||||
|
- D-12: `CloudResourceAdapter`, `reconcile_cloud_listing`, the connection-ID browse API, and `StorageBrowser.vue` remain the shared contracts. No parallel provider-specific browser, reconciliation path, or response schema may be introduced.
|
||||||
|
|
||||||
|
### Claude's Discretion
|
||||||
|
- Exact fixture organization and helper names.
|
||||||
|
- Whether listing validation is represented as a result type, domain exception, or validated `CloudListing` constructor, provided controlled errors and existing shared contracts are preserved.
|
||||||
|
- Whether the live test calls the adapter directly, the refresh task, or both, provided there is at least one end-to-end connection-ID browse assertion through DocuVault.
|
||||||
|
|
||||||
|
</decisions>
|
||||||
|
|
||||||
|
<canonical_refs>
|
||||||
|
## Canonical References
|
||||||
|
|
||||||
|
### Phase 12 architecture and validation
|
||||||
|
- `.planning/phases/12-cloud-resource-foundation/12-CONTEXT.md` — locked cloud resource architecture decisions.
|
||||||
|
- `.planning/phases/12-cloud-resource-foundation/12-VALIDATION.md` — existing provider, browse, security, and no-byte verification map.
|
||||||
|
- `.planning/phases/12-cloud-resource-foundation/12-SECURITY.md` — Phase 12 threat model and security evidence.
|
||||||
|
- `AGENTS.md` — non-negotiable shared-module, testing, security, environment, documentation, and Git rules.
|
||||||
|
|
||||||
|
### Shared cloud implementation
|
||||||
|
- `backend/storage/cloud_base.py` — normalized resource/listing/capability contract.
|
||||||
|
- `backend/storage/nextcloud_backend.py` — Nextcloud listing implementation.
|
||||||
|
- `backend/storage/webdav_backend.py` — generic WebDAV listing implementation.
|
||||||
|
- `backend/storage/google_drive_backend.py` — Google Drive listing implementation.
|
||||||
|
- `backend/storage/onedrive_backend.py` — OneDrive listing implementation.
|
||||||
|
- `backend/services/cloud_items.py` — sole metadata reconciliation entry point.
|
||||||
|
- `backend/tasks/cloud_tasks.py` — background refresh and freshness transitions.
|
||||||
|
- `backend/api/cloud/browse.py` — owner-scoped connection-ID cached-first browse API.
|
||||||
|
|
||||||
|
### Existing verification
|
||||||
|
- `backend/tests/test_cloud_backends.py` — provider normalization contract fixtures.
|
||||||
|
- `backend/tests/test_cloud_items.py` — reconciliation, refresh, retention, and no-byte tests.
|
||||||
|
- `backend/tests/test_cloud.py` — connection and browse API integration tests.
|
||||||
|
- `backend/tests/test_cloud_security.py` — owner/admin/credential/SSRF/no-byte negative suite.
|
||||||
|
|
||||||
|
</canonical_refs>
|
||||||
|
|
||||||
|
<specifics>
|
||||||
|
## Specific Ideas
|
||||||
|
|
||||||
|
- The observed symptom is internally contradictory: the UI says synchronization succeeded, yet the connected Nextcloud root renders no folders or files.
|
||||||
|
- The expected manifest intentionally includes both folders and files, names with spaces, multiple extensions, and mixed media/document types; it should catch root self-entry filtering, href decoding, and kind-normalization defects.
|
||||||
|
- The eventual test output should report only sanitized status, entry counts, and expected/missing/unexpected basenames.
|
||||||
|
|
||||||
|
</specifics>
|
||||||
|
|
||||||
|
<deferred>
|
||||||
|
## Deferred Ideas
|
||||||
|
|
||||||
|
- Upload, create-folder, rename, move, and delete behavior remain Phase 13.
|
||||||
|
- Byte download/cache and document analysis remain Phase 14.
|
||||||
|
- Production monitoring beyond truthful per-folder freshness remains Phase 16.
|
||||||
|
|
||||||
|
</deferred>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Phase: 12.1-fix-nextcloud-root-listing-and-sync-visibility*
|
||||||
|
*Context gathered: 2026-06-22*
|
||||||
+234
@@ -0,0 +1,234 @@
|
|||||||
|
# Phase 12.1 Pattern Map: Cloud Listing Correctness
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Map the existing shared patterns and exact change seams for making root and nested cloud listings correct across Nextcloud, generic WebDAV, Google Drive, and OneDrive. This is a static repository map only: no `.env` access, credentials, provider calls, or network operations were used.
|
||||||
|
|
||||||
|
The supplied Nextcloud root names are acceptance evidence, not configuration or test secrets:
|
||||||
|
|
||||||
|
- Folders: `Documents`, `docuvault`, `Photos`, `Templates`
|
||||||
|
- Files: `Nextcloud.png`, `Nextcloud Intro.mp4`, `Manual Nextcloud.png`, `Readme.md`, `Reasons to use Nextcloud.pdf`, `Template credits.md`
|
||||||
|
|
||||||
|
## Root-Cause Map
|
||||||
|
|
||||||
|
### 1. Nextcloud breaks the shared adapter contract
|
||||||
|
|
||||||
|
`backend/storage/cloud_base.py` defines the canonical interface:
|
||||||
|
|
||||||
|
```python
|
||||||
|
await adapter.list_folder(connection_id, user_id, parent_ref=None, page_token=None)
|
||||||
|
-> CloudListing
|
||||||
|
```
|
||||||
|
|
||||||
|
`backend/storage/webdav_backend.py` implements that interface and already contains the reusable WebDAV/Nextcloud PROPFIND normalization into `CloudResource` and `CloudListing`.
|
||||||
|
|
||||||
|
`backend/storage/nextcloud_backend.py`, however, overrides `list_folder` with the legacy signature `list_folder(folder_path="") -> list[dict]`. The canonical API and Celery task call it with connection/user UUIDs plus `parent_ref`, so Nextcloud can raise a signature error before listing. It also returns the legacy shape instead of `CloudListing`.
|
||||||
|
|
||||||
|
**Fix pattern:** do not add another Nextcloud listing implementation. Remove the incompatible override and inherit `WebDAVBackend.list_folder`, or move the legacy behavior behind an explicitly named compatibility method used only by the legacy route. The canonical `CloudResourceAdapter.list_folder` must have one signature and one return type for every provider.
|
||||||
|
|
||||||
|
### 2. The frontend consumes the legacy item shape
|
||||||
|
|
||||||
|
`backend/api/cloud/schemas.py` exposes canonical items as:
|
||||||
|
|
||||||
|
```text
|
||||||
|
id DocuVault stable UUID
|
||||||
|
provider_item_id provider folder/file reference
|
||||||
|
kind "folder" or "file"
|
||||||
|
```
|
||||||
|
|
||||||
|
`frontend/src/views/CloudFolderView.vue` still filters by `is_dir` and navigates with `item.id`. Consequently:
|
||||||
|
|
||||||
|
- canonical folders are not recognized as folders;
|
||||||
|
- nested navigation sends the DocuVault metadata UUID as `parent_ref` instead of the provider reference;
|
||||||
|
- this fails for every provider, not just Nextcloud.
|
||||||
|
|
||||||
|
**Fix pattern:** normalize once at the API/view boundary or consume the canonical schema directly. In `CloudFolderView.vue`, derive folders/files from `item.kind`, and navigate using `item.provider_item_id`. Do not teach `StorageBrowser.vue` provider-specific IDs or add provider branches.
|
||||||
|
|
||||||
|
### 3. The UI invents freshness instead of rendering provider state
|
||||||
|
|
||||||
|
`backend/api/cloud/browse.py` returns `freshness` from durable `CloudFolderState`, including `refresh_state`, `last_refreshed_at`, and controlled errors. `frontend/src/views/CloudFolderView.vue` ignores it and sets `freshness: 'fresh'` plus the browser's current time after every HTTP 200.
|
||||||
|
|
||||||
|
An empty listing caused by a provider failure can therefore appear synchronized.
|
||||||
|
|
||||||
|
**Fix pattern:** map `data.freshness.refresh_state` into the store's UI vocabulary and use `data.freshness.last_refreshed_at`; preserve `error_code/error_message` as the user-visible warning source. Never infer provider freshness from HTTP success.
|
||||||
|
|
||||||
|
### 4. Empty complete listings and failed listings must remain distinguishable
|
||||||
|
|
||||||
|
`CloudListing.complete` in `backend/storage/cloud_base.py` is the authoritative distinction:
|
||||||
|
|
||||||
|
- `complete=True`: full listing; reconciliation may soft-delete unseen children.
|
||||||
|
- `complete=False`: partial/failed listing; reconciliation must retain cached children.
|
||||||
|
|
||||||
|
`backend/services/cloud_items.py::reconcile_cloud_listing` already implements this correctly. Keep it as the only metadata reconciliation entry point. Adapter/API fixes must not bypass it or update `CloudItem` rows directly.
|
||||||
|
|
||||||
|
The synchronous first-load path in `backend/api/cloud/browse.py` should only mark a folder fresh after an authoritative result. A `CloudListing(complete=False)` must produce a controlled warning/stale state and must not be reported as fresh. Apply the same decision in `backend/tasks/cloud_tasks.py` so synchronous and background refreshes cannot disagree.
|
||||||
|
|
||||||
|
## Shared Pipeline and Exact Files
|
||||||
|
|
||||||
|
### Adapter contract and normalization
|
||||||
|
|
||||||
|
- `backend/storage/cloud_base.py`
|
||||||
|
- Preserve `CloudResourceAdapter.list_folder` as the sole browse contract.
|
||||||
|
- Preserve normalized `CloudResource` identity fields and `CloudListing.complete` semantics.
|
||||||
|
- If validation is strengthened, add provider-neutral checks here rather than in four adapters (for example, reject duplicate provider IDs within one authoritative page/listing).
|
||||||
|
- `backend/storage/webdav_backend.py`
|
||||||
|
- Keep one DAV listing implementation for both WebDAV and Nextcloud.
|
||||||
|
- Root is `parent_ref=None`, translated internally to the SDK's empty relative path.
|
||||||
|
- Continue returning provider-relative paths as `provider_item_id` and the requested parent reference as `parent_ref`.
|
||||||
|
- Keep SSRF revalidation before outbound calls and the no-byte-download invariant.
|
||||||
|
- `backend/storage/nextcloud_backend.py`
|
||||||
|
- Remove/rename the legacy `list_folder(folder_path)` override so the inherited canonical implementation is used.
|
||||||
|
- Retain only genuinely Nextcloud-specific behavior such as root health-check path conventions.
|
||||||
|
- Do not duplicate the PROPFIND normalization loop.
|
||||||
|
- `backend/storage/google_drive_backend.py`
|
||||||
|
- Preserve root translation `None -> "root"`, provider IDs for navigation, full pagination, and `complete=False` on provider failure.
|
||||||
|
- `backend/storage/onedrive_backend.py`
|
||||||
|
- Preserve root translation to `/me/drive/root/children`, item IDs for navigation, `@odata.nextLink` exhaustion, and incomplete failure semantics.
|
||||||
|
- `backend/storage/cloud_backend_factory.py`
|
||||||
|
- Continue constructing all providers behind `build_cloud_resource_adapter`.
|
||||||
|
- Add/retain a defensive contract assertion, but do not special-case list behavior by provider.
|
||||||
|
|
||||||
|
### Service and task patterns
|
||||||
|
|
||||||
|
- `backend/services/cloud_items.py`
|
||||||
|
- `resolve_owned_connection`: owner boundary before adapter access.
|
||||||
|
- `upsert_cloud_item`: stable metadata identity by `(connection_id, provider_item_id)`.
|
||||||
|
- `reconcile_cloud_listing`: only reconciliation entry point; soft-delete only for complete listings.
|
||||||
|
- `list_cloud_children`: query by the exact canonical `parent_ref` used during reconciliation.
|
||||||
|
- `update_folder_state`: canonical controlled freshness/error state; never raw provider exception text.
|
||||||
|
- `backend/tasks/cloud_tasks.py`
|
||||||
|
- Keep credential decryption inside the worker.
|
||||||
|
- Call only the canonical adapter method.
|
||||||
|
- Treat `complete=False` as refresh failure/warning rather than successful freshness.
|
||||||
|
- Preserve bounded retries, owner revalidation, metadata-only operation, and no quota mutation/download.
|
||||||
|
|
||||||
|
### API patterns
|
||||||
|
|
||||||
|
- `backend/api/cloud/browse.py`
|
||||||
|
- Keep `/api/cloud/connections/{connection_id}/items` as the canonical owner-scoped endpoint.
|
||||||
|
- Preserve whitelisted response conversion through `_item_out`, `_capability_out`, and `_freshness_out`.
|
||||||
|
- First visit may fetch synchronously, but must not mark an incomplete listing fresh.
|
||||||
|
- Cached visits continue stale-while-revalidate through `refresh_cloud_folder.delay`.
|
||||||
|
- Retire or isolate `_fetch_*_folders` compatibility helpers; do not let the legacy route define new listing semantics.
|
||||||
|
- `backend/api/cloud/connections.py`
|
||||||
|
- The `/folders/{provider}/{folder_id}` route is legacy. Migrate remaining consumers/tests to the connection-ID endpoint, then remove it when unreferenced rather than maintaining two provider listing pipelines.
|
||||||
|
- `backend/api/cloud/schemas.py`
|
||||||
|
- Keep the explicit credential-free schema.
|
||||||
|
- `kind` and `provider_item_id` are canonical; avoid restoring `is_dir` solely to accommodate stale frontend code unless a deliberate transitional compatibility field is documented and tested.
|
||||||
|
|
||||||
|
### Frontend patterns
|
||||||
|
|
||||||
|
- `frontend/src/api/cloud.js`
|
||||||
|
- Keep `getCloudFoldersByConnectionId(connectionId, parentRef)` as the single browse client.
|
||||||
|
- Preserve URL encoding of opaque/path-like provider references.
|
||||||
|
- Remove deprecated `getCloudFolders(provider, folderId)` once no active consumer remains.
|
||||||
|
- `frontend/src/views/CloudFolderView.vue`
|
||||||
|
- Remain a thin data provider.
|
||||||
|
- Split items using `kind` and navigate folders using `provider_item_id`.
|
||||||
|
- Feed backend freshness and controlled warning data into the store; do not synthesize “fresh”.
|
||||||
|
- Keep connection UUID routing and session-only folder memory.
|
||||||
|
- Do not add provider switches or listing layout.
|
||||||
|
- `frontend/src/stores/cloudConnections.js`
|
||||||
|
- Keep shared browse state here.
|
||||||
|
- If freshness translation is non-trivial, add one shared mapper/action here and test it once; views should not each invent status semantics.
|
||||||
|
- `frontend/src/components/storage/StorageBrowser.vue`
|
||||||
|
- Remain the only local/cloud file browser.
|
||||||
|
- Receive already normalized folders/files and capability/freshness props.
|
||||||
|
- No provider-specific path parsing or Nextcloud-only rendering.
|
||||||
|
|
||||||
|
## Test Design
|
||||||
|
|
||||||
|
### Provider contract suite: one parametrized contract, provider fixtures only
|
||||||
|
|
||||||
|
Extend `backend/tests/test_cloud_backends.py` with a shared adapter contract harness covering Google Drive, OneDrive, WebDAV, and Nextcloud. Each provider supplies mocked SDK/HTTP responses; assertions stay provider-neutral:
|
||||||
|
|
||||||
|
1. Root call accepts `(connection_id, user_id, parent_ref=None)` and returns `CloudListing`.
|
||||||
|
2. Every child is a `CloudResource` with the supplied connection/user IDs.
|
||||||
|
3. Folder/file classification, names, provider IDs, parent refs, sizes, MIME types, timestamps, and etags normalize correctly.
|
||||||
|
4. Nested listing accepts a returned folder's `provider_item_id` as the next `parent_ref`.
|
||||||
|
5. Pagination is exhausted before `complete=True` (where applicable).
|
||||||
|
6. Provider/network failure returns or is translated to `complete=False` without byte downloads or mutation calls.
|
||||||
|
7. No adapter invokes `get_object`, upload, delete, move, rename, or quota code while browsing.
|
||||||
|
|
||||||
|
Add an explicit Nextcloud regression in `backend/tests/test_cloud_backends.py`:
|
||||||
|
|
||||||
|
- instantiate `NextcloudBackend` with mocked `webdav3` client;
|
||||||
|
- call the canonical four-argument adapter method;
|
||||||
|
- return the ten supplied root entries from mocked PROPFIND metadata;
|
||||||
|
- assert exactly four normalized folders and six normalized files by the expected names;
|
||||||
|
- assert root children have `parent_ref is None`;
|
||||||
|
- select `Documents` and prove its `provider_item_id` can be passed back for a nested listing.
|
||||||
|
|
||||||
|
This test uses only expected names as fixture evidence. It must not contain a URL, username, app-password, encrypted credentials, or network marker.
|
||||||
|
|
||||||
|
### Service reconciliation suite
|
||||||
|
|
||||||
|
Extend `backend/tests/test_cloud_items.py`:
|
||||||
|
|
||||||
|
- authoritative root listing upserts all ten unique Nextcloud fixture names;
|
||||||
|
- repeating the listing is idempotent;
|
||||||
|
- renamed/moved metadata preserves DocuVault item UUID by provider ID;
|
||||||
|
- `complete=False` retains cached rows and records no deletions;
|
||||||
|
- empty `complete=True` is allowed to remove known children, proving failure and true-empty semantics remain distinct;
|
||||||
|
- root (`None`) and nested parent refs never collide across connection/user boundaries.
|
||||||
|
|
||||||
|
### Task suite
|
||||||
|
|
||||||
|
Extend `backend/tests/test_cloud_items.py` or create `backend/tests/test_cloud_tasks.py` if task behavior becomes substantial:
|
||||||
|
|
||||||
|
- Nextcloud adapter receives canonical arguments through `_run`;
|
||||||
|
- complete listing reconciles and marks fresh;
|
||||||
|
- incomplete listing marks warning/stale and does not delete cached items;
|
||||||
|
- transient exception retries, terminal auth failure does not retry;
|
||||||
|
- credentials stay out of broker payload and logs;
|
||||||
|
- no byte or quota APIs are called.
|
||||||
|
|
||||||
|
### API integration suite
|
||||||
|
|
||||||
|
Extend `backend/tests/test_cloud.py` and `backend/tests/test_cloud_security.py`:
|
||||||
|
|
||||||
|
- first root browse with a mocked provider contract returns all normalized items;
|
||||||
|
- response exposes `kind` and `provider_item_id`, never credentials;
|
||||||
|
- incomplete first fetch returns controlled non-fresh status rather than a false fresh status;
|
||||||
|
- cached listing is returned while refresh is scheduled;
|
||||||
|
- nested browse forwards the exact provider reference;
|
||||||
|
- all four provider values pass through the same endpoint;
|
||||||
|
- wrong-owner/admin requests remain 404/403 and never invoke adapters;
|
||||||
|
- browse never downloads bytes or mutates quota.
|
||||||
|
|
||||||
|
### Frontend suite
|
||||||
|
|
||||||
|
Extend `frontend/src/views/__tests__/CloudFolderView.test.js`:
|
||||||
|
|
||||||
|
- canonical `kind: 'folder'` and `kind: 'file'` entries are passed to `StorageBrowser` in the right props;
|
||||||
|
- clicking `Documents` routes with its `provider_item_id`, not its DocuVault `id`;
|
||||||
|
- root request omits/translates the `root` sentinel consistently to canonical `parent_ref=None` at the API boundary;
|
||||||
|
- backend `freshness.refresh_state`, timestamp, and warning message populate store/UI state;
|
||||||
|
- HTTP 200 with warning/incomplete state never becomes “fresh” merely because the request resolved;
|
||||||
|
- expected ten-name Nextcloud fixture is rendered through the same shared browser as every provider.
|
||||||
|
|
||||||
|
Extend `frontend/src/stores/__tests__/cloudConnections.test.js` if freshness mapping is placed in the store. Preserve existing connection UUID and session-storage secrecy tests.
|
||||||
|
|
||||||
|
## Acceptance Matrix
|
||||||
|
|
||||||
|
| Layer | Provider-neutral acceptance |
|
||||||
|
|---|---|
|
||||||
|
| Adapter | All four providers implement the exact `CloudResourceAdapter` signature and return `CloudListing` |
|
||||||
|
| Root identity | UI root sentinel becomes backend `parent_ref=None`; adapters translate root internally |
|
||||||
|
| Child identity | `provider_item_id` is used for provider navigation; `id` remains DocuVault metadata identity |
|
||||||
|
| Classification | `kind` is the sole canonical folder/file discriminator |
|
||||||
|
| Reconciliation | Only `reconcile_cloud_listing` writes listing metadata; incomplete results never delete cached rows |
|
||||||
|
| Freshness | Backend folder state is authoritative; incomplete/error results cannot display as fresh |
|
||||||
|
| Security | Owner scoping, credential secrecy, SSRF checks, no byte download, and no quota mutation remain intact |
|
||||||
|
| Nextcloud evidence | Root shows exactly the four supplied folders and six supplied files under mocked acceptance data |
|
||||||
|
|
||||||
|
## Implementation Order
|
||||||
|
|
||||||
|
1. Add the shared provider contract tests and failing Nextcloud signature/root regression.
|
||||||
|
2. Remove the Nextcloud legacy override in favor of inherited DAV normalization.
|
||||||
|
3. Make incomplete-listing handling consistent in synchronous API and background task paths.
|
||||||
|
4. Fix `CloudFolderView.vue` to use `kind`, `provider_item_id`, and backend freshness.
|
||||||
|
5. Add API and frontend regressions for root, nested navigation, warning truthfulness, and all-provider parity.
|
||||||
|
6. Remove the legacy provider-slug browse path/helpers when repository search proves no active consumer remains.
|
||||||
|
|
||||||
|
No provider-specific file grid, reconciliation function, freshness mapper, or Nextcloud-only listing parser should be introduced.
|
||||||
+333
@@ -0,0 +1,333 @@
|
|||||||
|
# Phase 12.1: Fix Nextcloud Root Listing and Sync Visibility — Research
|
||||||
|
|
||||||
|
**Researched:** 2026-06-22
|
||||||
|
**Status:** Complete
|
||||||
|
**Scope:** Secure live diagnosis, provider-neutral browse contracts, reconciliation/freshness correctness, and cross-provider validation design
|
||||||
|
|
||||||
|
## Research Question
|
||||||
|
|
||||||
|
Why can a healthy Nextcloud connection report a successful sync while its root appears empty, and what contract, integration, live-smoke, and UI tests are required to make browse correctness observable across Nextcloud, generic WebDAV, Google Drive, and OneDrive without downloading provider bytes or mutating provider content?
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
The live Nextcloud account is reachable and its root is not empty. A credential-redacted, read-only `PROPFIND` with `Depth: 1` returned HTTP `207 Multi-Status` and 10 direct children. Seven names exactly matched the supplied expectation and three returned names differed from the supplied expectation; no unexpected names were printed or persisted. This proves that the configured account and WebDAV endpoint can return root metadata and that the DocuVault empty-root behavior is primarily an application defect, not an empty account or basic connectivity failure. The three name differences should be treated as test-fixture drift until confirmed in the Nextcloud UI; they do not explain DocuVault returning no items.
|
||||||
|
|
||||||
|
The immediate backend root cause is deterministic: `NextcloudBackend` overrides the provider-neutral `WebDAVBackend.list_folder(connection_id, user_id, parent_ref=None, page_token=None) -> CloudListing` with a legacy `list_folder(folder_path="") -> list[dict]`. The canonical browse endpoint invokes the four-argument contract, so a Nextcloud adapter raises `TypeError` before performing a listing. Existing tests only assert that Nextcloud is a `CloudResourceAdapter` subclass and that `list_folder` is async; they never invoke the Nextcloud instance through the canonical contract. Python inheritance therefore made a structurally invalid adapter look valid.
|
||||||
|
|
||||||
|
Three additional defects create false-sync or invisible-navigation behavior across providers:
|
||||||
|
|
||||||
|
1. The synchronous browse path and Celery refresh path mark a folder `fresh` even when an adapter returns `CloudListing(complete=False)`. An incomplete or failed fetch is therefore eligible to look successfully synchronized.
|
||||||
|
2. `CloudFolderView.vue` ignores `response.freshness`, unconditionally sets freshness to `fresh`, and invents `lastRefreshedAt` from the browser clock after any HTTP 200. A safe HTTP response containing an empty incomplete/warning listing can therefore be shown as current.
|
||||||
|
3. The API returns `kind: "folder" | "file"`, `provider_item_id`, and a stable DocuVault `id`, but cloud views and tree components still filter on `is_dir` and navigate using `item.id`. Folders are consequently treated as files, and folder navigation sends the DocuVault metadata UUID where adapters require the provider folder reference/path.
|
||||||
|
|
||||||
|
Phase 12.1 should fix the inherited adapter contract first, then make listing completeness control freshness, then align the frontend with the normalized API. The verification architecture should apply one reusable provider contract suite to all four adapters and add opt-in, read-only live smoke tests whose credentials remain in ignored environment variables.
|
||||||
|
|
||||||
|
## Secure Live Probe Evidence
|
||||||
|
|
||||||
|
### Guardrails Used
|
||||||
|
|
||||||
|
- Read only the ignored `.env` variables `NEXTCLOUD_URL`, `NEXTCLOUD_USER`, and `NEXTCLOUD_APP_PASSWORD` in process memory.
|
||||||
|
- Performed only one-level `PROPFIND` metadata requests; no `GET`, download, upload, `MKCOL`, `PUT`, `MOVE`, `COPY`, or `DELETE` request was made.
|
||||||
|
- Did not edit `.env` or any environment file.
|
||||||
|
- Did not print, echo, log, quote, persist, or include credentials or the full URL in this artifact.
|
||||||
|
- Reported only response status, direct-child count, expected-match count, missing expected names, and unexpected-name count. Unexpected provider names were deliberately not emitted.
|
||||||
|
|
||||||
|
### Sanitized Result
|
||||||
|
|
||||||
|
| Check | Result |
|
||||||
|
|---|---|
|
||||||
|
| Endpoint reachable | Yes |
|
||||||
|
| Authentication accepted | Yes |
|
||||||
|
| Method | `PROPFIND` |
|
||||||
|
| Depth | `1` |
|
||||||
|
| HTTP status | `207 Multi-Status` |
|
||||||
|
| Direct root children returned | 10 |
|
||||||
|
| Exact supplied-name matches | 7 of 10 |
|
||||||
|
| Supplied names not exactly matched | `Manual Nextcloud.png`, `Nextcloud Intro.mp4`, `Template credits.md` |
|
||||||
|
| Additional returned-name count | 3; names withheld |
|
||||||
|
| Byte download or provider mutation | None |
|
||||||
|
|
||||||
|
The root-list response confirms that URL derivation to the user WebDAV root works when a base Nextcloud URL is normalized to the standard `/remote.php/dav/files/{encoded-user}/` endpoint. Repeated percent-decoding and plus-decoding did not turn the three differences into exact matches, so those differences are not explained by a simple href percent-encoding bug.
|
||||||
|
|
||||||
|
## Root-Cause Analysis
|
||||||
|
|
||||||
|
### P0 — Nextcloud Violates the Provider-Neutral Adapter Signature
|
||||||
|
|
||||||
|
Current runtime signatures:
|
||||||
|
|
||||||
|
```text
|
||||||
|
NextcloudBackend.list_folder(self, folder_path: str = "") -> list[dict]
|
||||||
|
WebDAVBackend.list_folder(self, connection_id, user_id, parent_ref=None, page_token=None) -> CloudListing
|
||||||
|
```
|
||||||
|
|
||||||
|
`build_cloud_resource_adapter("nextcloud", credentials)` passes the `isinstance(..., CloudResourceAdapter)` check because `NextcloudBackend` inherits from `WebDAVBackend`. However, the override replaces the abstract-contract implementation with the old pre-Phase-12 method. The canonical browse endpoint calls:
|
||||||
|
|
||||||
|
```python
|
||||||
|
await adapter.list_folder(connection_id, current_user.id, parent_ref=parent_ref)
|
||||||
|
```
|
||||||
|
|
||||||
|
For Nextcloud this raises before the provider request. The most direct fix is to delete the duplicate Nextcloud `list_folder` override and inherit the shared WebDAV implementation. If Nextcloud-specific href normalization is required, it should be a narrowly scoped protected hook used by the shared WebDAV listing algorithm, not a second public method with a different contract.
|
||||||
|
|
||||||
|
This is exactly the project rule “things that look the same to the user are the same in code”: Nextcloud is a WebDAV specialization and must not own a parallel browse API.
|
||||||
|
|
||||||
|
### P0 — Incomplete Listings Are Marked Fresh
|
||||||
|
|
||||||
|
Both first-visit browse reconciliation and `refresh_cloud_folder` do the following regardless of `listing.complete`:
|
||||||
|
|
||||||
|
1. reconcile the listing;
|
||||||
|
2. set folder state to `fresh`;
|
||||||
|
3. update `last_refreshed_at`.
|
||||||
|
|
||||||
|
`reconcile_cloud_listing` correctly avoids soft deletion for `complete=False`, but freshness semantics are not enforced by the service boundary. An incomplete empty result can therefore preserve cached rows yet still claim synchronization succeeded; on first visit it can claim an empty root is fresh.
|
||||||
|
|
||||||
|
The planner should introduce one service-level completion gate shared by synchronous browse and Celery refresh. A listing may transition to `fresh` only when `complete=True` and every required page/response was parsed successfully. `complete=False` must retain the previous `last_refreshed_at`, set a controlled warning/error code, and schedule bounded retry where appropriate. An empty `complete=True` listing remains a valid authoritative empty folder.
|
||||||
|
|
||||||
|
### P0 — Frontend Discards Server Freshness
|
||||||
|
|
||||||
|
`CloudFolderView.vue` currently changes state to:
|
||||||
|
|
||||||
|
```text
|
||||||
|
freshness = "fresh"
|
||||||
|
refreshedAt = new Date().toISOString()
|
||||||
|
```
|
||||||
|
|
||||||
|
after every successful HTTP response. It does not use `data.freshness.refresh_state`, `last_refreshed_at`, `error_code`, or `error_message`. This independently explains the user-visible “everything is synced” report even when backend refresh failed or was incomplete.
|
||||||
|
|
||||||
|
The view must map the server freshness object verbatim into the store and render controlled warning copy. The client clock must never manufacture provider refresh evidence. A 200 response means the cached browse request succeeded; it does not mean provider synchronization succeeded.
|
||||||
|
|
||||||
|
### P0 — API/UI Item Shape and Navigation Reference Disagree
|
||||||
|
|
||||||
|
The backend whitelisted response uses:
|
||||||
|
|
||||||
|
```text
|
||||||
|
kind: "folder" | "file"
|
||||||
|
id: stable DocuVault CloudItem UUID
|
||||||
|
provider_item_id: provider folder/file reference
|
||||||
|
```
|
||||||
|
|
||||||
|
The frontend still expects `is_dir` and uses `item.id` as the next `parent_ref`. Consequences:
|
||||||
|
|
||||||
|
- `items.filter(i => i.is_dir)` yields no folders because `is_dir` is absent;
|
||||||
|
- folders fall into the file collection;
|
||||||
|
- sidebar trees also filter on the absent field;
|
||||||
|
- clicking a folder, where possible, sends a DocuVault UUID to WebDAV/Drive/Graph instead of the provider reference.
|
||||||
|
|
||||||
|
Prefer one normalized frontend shape rather than adding duplicate backend fields. Views/components should use `item.kind === "folder"`, and provider navigation should use `item.provider_item_id`. The stable DocuVault `id` remains the row identity/key for persistence and future analysis. If the team chooses to expose an explicit `navigation_ref`, it must be populated centrally in `CloudItemOut`; do not reconstruct provider-specific navigation values in Vue.
|
||||||
|
|
||||||
|
### P1 — Nextcloud URL Normalization Is UI-Only
|
||||||
|
|
||||||
|
The credential modal constructs the standard Nextcloud WebDAV root from a base URL and encoded username, but the backend accepts and stores an arbitrary `server_url` without provider-specific normalization. This creates inconsistent behavior among UI-created connections, API clients, imported configuration, edited credentials, and live tests.
|
||||||
|
|
||||||
|
Add one pure shared helper for Nextcloud endpoint normalization, used by connection create/update and backend construction. Required cases:
|
||||||
|
|
||||||
|
- bare origin with or without trailing slash;
|
||||||
|
- origin with a deployment subpath;
|
||||||
|
- already canonical `/remote.php/dav/files/{user}/` endpoint;
|
||||||
|
- username requiring path-segment percent encoding;
|
||||||
|
- trailing-slash idempotence;
|
||||||
|
- rejection of query strings, fragments, userinfo, non-HTTPS, private/loopback/link-local targets, and malformed endpoints;
|
||||||
|
- no double append of the WebDAV suffix;
|
||||||
|
- no logging of the normalized URL when it contains a username path segment.
|
||||||
|
|
||||||
|
SSRF validation must run against the normalized URL before client construction and before every outbound request. Redirects must not bypass host/IP revalidation.
|
||||||
|
|
||||||
|
### P1 — WebDAV Listing Uses N+1 Requests and Weak Error Semantics
|
||||||
|
|
||||||
|
The shared implementation calls `client.list()` and then `client.info()` once per item. Per-item `info()` errors are swallowed and converted to defaults, potentially misclassifying a folder as a file. A standards-level `PROPFIND Depth: 1` can return the root self-response and all direct children with `resourcetype`, length, type, mtime, and etag in one response.
|
||||||
|
|
||||||
|
Phase 12.1 should preferably centralize a single Depth-1 parser for Nextcloud and generic WebDAV. It must:
|
||||||
|
|
||||||
|
- send explicit `Depth: 1`;
|
||||||
|
- request an allowlist of metadata properties only;
|
||||||
|
- identify and exclude exactly the collection self-response by normalized href, not by a display-name coincidence;
|
||||||
|
- decode each href path segment exactly once for display while retaining a canonical provider reference for subsequent requests;
|
||||||
|
- accept absolute and relative hrefs but reject a child href that escapes the configured root;
|
||||||
|
- distinguish folders from the DAV `collection` resource type, not size or trailing slash alone;
|
||||||
|
- treat missing optional properties as `None` without dropping the item;
|
||||||
|
- mark the entire listing incomplete on malformed multistatus, request failure, untrusted href, or interrupted pagination/response processing;
|
||||||
|
- never call byte-content methods.
|
||||||
|
|
||||||
|
## Provider-Neutral Correctness Contract
|
||||||
|
|
||||||
|
Every provider adapter must pass the same behavioral suite. Provider-specific fixtures are inputs; normalized `CloudListing` and `CloudResource` behavior is the contract.
|
||||||
|
|
||||||
|
### Adapter Contract
|
||||||
|
|
||||||
|
For each of Nextcloud, WebDAV, Google Drive, and OneDrive:
|
||||||
|
|
||||||
|
1. `list_folder` has the canonical callable signature and accepts root plus nested `parent_ref`.
|
||||||
|
2. It returns `CloudListing`, never a provider dictionary/list.
|
||||||
|
3. Every resource contains the requested `connection_id` and `user_id` supplied by the trusted caller.
|
||||||
|
4. `provider_item_id` is a usable opaque navigation reference; `id` is a generated/stable-in-DB DocuVault identity and is not sent back to the provider.
|
||||||
|
5. `kind`, `name`, parent, size, content type, modified time, and etag/version normalize consistently, with absent provider metadata represented as `None`.
|
||||||
|
6. All pages are consumed before `complete=True`; an error on any page yields `complete=False` and must not authorize deletion.
|
||||||
|
7. Authentication/scope failures map to typed, controlled domain reasons; raw provider bodies and URLs never reach API responses or audit logs.
|
||||||
|
8. Listing and capability discovery invoke no upload, create-folder, move, rename, copy, delete, media/content, or download method.
|
||||||
|
9. Listing changes neither `quotas.used_bytes` nor MinIO/object storage.
|
||||||
|
|
||||||
|
### Provider-Specific Fixtures
|
||||||
|
|
||||||
|
| Provider | Required fixture assertions |
|
||||||
|
|---|---|
|
||||||
|
| Nextcloud | Canonical and base URL normalization; `207` multistatus; explicit Depth 1; root self href removed; encoded spaces/unicode; DAV collection detection; nested path; malformed/foreign href incomplete; canonical four-argument method inherited or implemented exactly once |
|
||||||
|
| Generic WebDAV | Absolute/relative hrefs; missing optional props; servers with/without trailing slashes; 401/403 vs 5xx mapping; root and nested Depth-1 listings; no N+1 metadata calls if direct parser is adopted |
|
||||||
|
| Google Drive | Root query uses `root`; every `nextPageToken` followed; folder MIME type; native Google files have nullable size; trashed items excluded; insufficient `drive.file` visibility represented as scope limitation where detectable; no `get_media` |
|
||||||
|
| OneDrive | Root and item children endpoints; every `@odata.nextLink` followed; folder/file facets; parent reference and drive context retained; eTag/cTag normalization; expired token refresh failure controlled; no `/content` request |
|
||||||
|
|
||||||
|
## Test Design
|
||||||
|
|
||||||
|
### 1. Static and Runtime Contract Regression
|
||||||
|
|
||||||
|
Add a parametrized test over all factory providers that constructs the adapter, inspects `list_folder`, and invokes it with `(connection_id, user_id, parent_ref=None, page_token=None)` against mocked provider responses. This would have caught the current Nextcloud override immediately. Merely testing `issubclass` or `iscoroutinefunction` is insufficient.
|
||||||
|
|
||||||
|
Suggested location: `backend/tests/test_cloud_provider_contract.py`.
|
||||||
|
|
||||||
|
Core assertions:
|
||||||
|
|
||||||
|
```text
|
||||||
|
factory returns CloudResourceAdapter
|
||||||
|
canonical invocation does not raise TypeError
|
||||||
|
return type is CloudListing
|
||||||
|
item ownership IDs equal trusted call arguments
|
||||||
|
listing uses provider metadata only
|
||||||
|
no byte or mutation spy was called
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. WebDAV/Nextcloud Parser Tests
|
||||||
|
|
||||||
|
Use recorded synthetic XML fixtures with no real credentials or hostnames:
|
||||||
|
|
||||||
|
- root self + 4 folders + 6 files produces exactly 10 children;
|
||||||
|
- spaces, `%` characters, unicode, trailing slash, absolute href, and relative href;
|
||||||
|
- child folder recognized from `<d:collection/>` even with size absent;
|
||||||
|
- same basename as root is not incorrectly filtered when it is a genuine child;
|
||||||
|
- href outside the configured DAV root is rejected and listing is incomplete;
|
||||||
|
- missing property status blocks do not drop a child;
|
||||||
|
- malformed XML, non-207 response, timeout, and mid-parse failure return/raise a controlled incomplete result;
|
||||||
|
- request method is `PROPFIND`, `Depth` is exactly `1`, and body requests no content bytes.
|
||||||
|
|
||||||
|
### 3. Freshness/Reconciliation Tests
|
||||||
|
|
||||||
|
Add a shared service such as `apply_cloud_listing_result` or an equivalent explicit branch and test:
|
||||||
|
|
||||||
|
- `complete=True`, non-empty: upsert, delete unseen siblings, state fresh, advance `last_refreshed_at`;
|
||||||
|
- `complete=True`, empty: authoritative empty folder, delete unseen siblings, state fresh;
|
||||||
|
- `complete=False`, empty or partial: retain all unseen rows, state warning, preserve previous `last_refreshed_at`;
|
||||||
|
- provider exception: retain cache, controlled warning, bounded retry only for transient errors;
|
||||||
|
- first visit + incomplete empty listing: response is empty with warning, never fresh;
|
||||||
|
- repeated complete listing is idempotent;
|
||||||
|
- no branch changes quota or calls byte storage.
|
||||||
|
|
||||||
|
Exercise both the synchronous first-visit endpoint and the Celery `_run` path so they cannot drift.
|
||||||
|
|
||||||
|
### 4. API and Frontend Contract Tests
|
||||||
|
|
||||||
|
Backend API tests should return a mixed folder/file fixture and assert `kind`, stable `id`, provider navigation reference, freshness, credential exclusion, owner scoping, and admin denial.
|
||||||
|
|
||||||
|
Frontend tests should use the real response shape, not `{items: []}`:
|
||||||
|
|
||||||
|
- mixed items split correctly using `kind`;
|
||||||
|
- folder click navigates with `provider_item_id` or centralized `navigation_ref`, never stable metadata `id`;
|
||||||
|
- sidebar tree applies the same rule;
|
||||||
|
- warning/refreshing/fresh is copied from `data.freshness`;
|
||||||
|
- `last_refreshed_at` comes from the server and is not replaced with `new Date()`;
|
||||||
|
- HTTP 200 + warning + empty items renders a synchronization warning, not “This folder is empty” with a fresh indicator;
|
||||||
|
- cached items remain visible during warning;
|
||||||
|
- filenames render as text with no `v-html`.
|
||||||
|
|
||||||
|
Suggested files:
|
||||||
|
|
||||||
|
- `frontend/src/views/__tests__/CloudFolderView.test.js`
|
||||||
|
- `frontend/src/components/cloud/__tests__/CloudProviderTreeItem.test.js`
|
||||||
|
- `frontend/src/components/cloud/__tests__/CloudFolderTreeItem.test.js`
|
||||||
|
- `frontend/src/stores/__tests__/cloudConnections.test.js`
|
||||||
|
|
||||||
|
### 5. Opt-In Read-Only Live Tests
|
||||||
|
|
||||||
|
Live tests should be marked, excluded from the default deterministic suite, and enabled explicitly, for example with `RUN_CLOUD_LIVE_TESTS=1`. They must skip when provider-specific variables are absent. Test output must contain provider name, status category, counts, and sanitized mismatch summaries only—never credentials, tokens, usernames, response headers, response bodies, or full URLs.
|
||||||
|
|
||||||
|
Nextcloud variables already available:
|
||||||
|
|
||||||
|
```text
|
||||||
|
NEXTCLOUD_URL
|
||||||
|
NEXTCLOUD_USER
|
||||||
|
NEXTCLOUD_APP_PASSWORD
|
||||||
|
```
|
||||||
|
|
||||||
|
The Nextcloud live test should normalize the endpoint through production code, invoke `build_cloud_resource_adapter("nextcloud", ...)`, call the canonical root listing, and assert:
|
||||||
|
|
||||||
|
- `CloudListing.complete is True`;
|
||||||
|
- exactly 10 direct children for the controlled fixture account, once fixture drift is reconciled;
|
||||||
|
- the agreed expected-name set matches exactly;
|
||||||
|
- at least the expected folder/file kinds are correct;
|
||||||
|
- no content or mutation method is available to the test harness;
|
||||||
|
- a quota snapshot before/after is unchanged if exercised through the DocuVault API.
|
||||||
|
|
||||||
|
For Google Drive, OneDrive, and generic WebDAV, define equivalent optional environment contracts only when dedicated revocable test accounts are supplied. OAuth live tests should list a controlled root folder with least-privilege test credentials, follow pagination, and compare an agreed fixture set. Never make the live suite create its own marker file: provider mutations are outside Phase 12.1 research and are prohibited for these smoke tests.
|
||||||
|
|
||||||
|
### 6. Security-Negative Tests
|
||||||
|
|
||||||
|
- foreign user connection ID returns indistinguishable 404;
|
||||||
|
- admin cannot browse user cloud metadata;
|
||||||
|
- credentials and normalized full endpoint never appear in response, logs, task payload, exception strings, or snapshots;
|
||||||
|
- worker decrypts credentials only after owner-scoped connection resolution;
|
||||||
|
- WebDAV/Nextcloud root and every redirect/request pass SSRF checks;
|
||||||
|
- malicious href cannot escape configured root or cause a request to another origin;
|
||||||
|
- provider error text is mapped to controlled codes/messages;
|
||||||
|
- listing performs no `get_object`, Google `get_media`, OneDrive `/content`, MinIO operation, quota update, or mutation verb.
|
||||||
|
|
||||||
|
## Recommended Implementation Shape
|
||||||
|
|
||||||
|
### Plan 12.1-01 — Restore the Shared Adapter Contract
|
||||||
|
|
||||||
|
- Add failing cross-provider runtime contract tests first.
|
||||||
|
- Remove the legacy Nextcloud public `list_folder` override so Nextcloud inherits the shared canonical WebDAV implementation.
|
||||||
|
- Resolve the deprecated provider-folder helper: route it through the canonical connection-ID adapter or delete the dead compatibility route if no active route/import depends on it. Do not retain two public listing shapes.
|
||||||
|
- Add backend Nextcloud URL normalization in one shared helper and test idempotence/security cases.
|
||||||
|
|
||||||
|
### Plan 12.1-02 — Make WebDAV Metadata Listing Authoritative
|
||||||
|
|
||||||
|
- Implement or encapsulate a single Depth-1 PROPFIND parser shared by Nextcloud and generic WebDAV.
|
||||||
|
- Normalize root self filtering, href containment/decoding, kind, metadata, and controlled errors.
|
||||||
|
- Add fixture tests for the supplied 10-item root plus encoding, missing-property, malformed-response, and SSRF cases.
|
||||||
|
- Keep listing metadata-only and mutation-free.
|
||||||
|
|
||||||
|
### Plan 12.1-03 — Eliminate False Freshness
|
||||||
|
|
||||||
|
- Centralize complete/incomplete listing state transitions.
|
||||||
|
- Use the same transition in synchronous browse and Celery refresh.
|
||||||
|
- Ensure incomplete results preserve cache and last success, set warning, and retry only when transient.
|
||||||
|
- Add reconciliation, endpoint, worker, quota, and no-byte tests.
|
||||||
|
|
||||||
|
### Plan 12.1-04 — Align the Shared Browser Contract
|
||||||
|
|
||||||
|
- Update cloud views/trees to use normalized `kind` and provider navigation reference.
|
||||||
|
- Consume server freshness instead of manufacturing success.
|
||||||
|
- Add mixed-item navigation and warning-state frontend regressions while keeping `StorageBrowser.vue` as the sole browser.
|
||||||
|
- Run the read-only Nextcloud live smoke, then full backend/frontend/security gates and documentation/version closeout required by project protocol.
|
||||||
|
|
||||||
|
## Acceptance Evidence
|
||||||
|
|
||||||
|
Phase 12.1 is complete only when all of the following hold:
|
||||||
|
|
||||||
|
1. The production Nextcloud adapter, invoked through `build_cloud_resource_adapter`, returns the agreed root fixture through the canonical contract.
|
||||||
|
2. Nextcloud and generic WebDAV share one public listing implementation/signature.
|
||||||
|
3. All four providers pass one runtime adapter contract suite and provider-specific normalization/pagination tests.
|
||||||
|
4. `complete=False` cannot produce `fresh` or advance `last_refreshed_at` in either request or worker paths.
|
||||||
|
5. The frontend renders folders/files from the API's normalized shape, navigates with provider references, and displays backend freshness faithfully.
|
||||||
|
6. Browse/live tests use metadata requests only and prove zero byte downloads, provider mutations, MinIO writes, and quota changes.
|
||||||
|
7. Owner/admin/credential/SSRF/href-containment negative tests pass.
|
||||||
|
8. The controlled Nextcloud live smoke reports a complete listing and the final agreed exact-name fixture; any fixture drift is explicitly reconciled rather than hidden.
|
||||||
|
|
||||||
|
## Risks and Planner Notes
|
||||||
|
|
||||||
|
- Do not “fix” Nextcloud by adding an adapter-specific branch in `browse.py` or Vue. The public contract must be identical across providers.
|
||||||
|
- Do not interpret `HTTP 200` from DocuVault as provider freshness; cached browse success and provider refresh success are separate states.
|
||||||
|
- Do not use a root item count alone as general production correctness. Exact names are appropriate only for dedicated controlled live accounts; generic folders may legitimately be empty.
|
||||||
|
- Do not mark an empty listing incomplete merely because it is empty. Completeness comes from successful authoritative traversal, not item count.
|
||||||
|
- Google `drive.file` can produce a valid but intentionally restricted view. Provider-neutral tests should distinguish transport completeness from authorization scope and expose `insufficient_scope` where known.
|
||||||
|
- OneDrive continuation URLs are provider-supplied. Follow them only for the expected Graph origin and never accept arbitrary client-supplied pagination URLs.
|
||||||
|
- Keep unexpected live filenames out of CI logs; report hashed/set counts or controlled expected-name diffs only.
|
||||||
|
- The current worktree contains unrelated user changes and untracked files. Implementation plans must stage only Phase 12.1 files plus required documentation/version updates.
|
||||||
|
|
||||||
|
## RESEARCH COMPLETE
|
||||||
@@ -61,7 +61,12 @@ blocked: 0
|
|||||||
reason: "User reported: All 'modified' rows are empty and just with a '-' filled. The native cloud storage browser does show a modified time."
|
reason: "User reported: All 'modified' rows are empty and just with a '-' filled. The native cloud storage browser does show a modified time."
|
||||||
severity: major
|
severity: major
|
||||||
test: 6
|
test: 6
|
||||||
root_cause: ""
|
root_cause: "StorageBrowser.vue renders formatDate(folder.created_at) and formatDate(file.created_at) (lines 170, 239). Cloud items have modified_at (not created_at) — the field exists in CloudItemOut and is mapped by the backend, but the template reads the wrong key."
|
||||||
artifacts: []
|
artifacts:
|
||||||
missing: []
|
- path: "frontend/src/components/storage/StorageBrowser.vue"
|
||||||
|
issue: "Lines 170 and 239 use created_at; cloud items populate modified_at instead"
|
||||||
|
- path: "backend/api/cloud/schemas.py"
|
||||||
|
issue: "CloudItemOut.modified_at exists and is correctly populated — no backend change needed"
|
||||||
|
missing:
|
||||||
|
- "StorageBrowser must render modified_at for cloud items (prop or computed field)"
|
||||||
debug_session: ""
|
debug_session: ""
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
---
|
||||||
|
phase: "13"
|
||||||
|
plan: "01"
|
||||||
|
type: tdd
|
||||||
|
wave: 0
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- backend/tests/test_cloud_mutations.py
|
||||||
|
- backend/tests/test_cloud_reconnect.py
|
||||||
|
- backend/tests/test_cloud_audit.py
|
||||||
|
- backend/tests/test_cloud_backends.py
|
||||||
|
- backend/tests/test_cloud_provider_contract.py
|
||||||
|
autonomous: true
|
||||||
|
requirements:
|
||||||
|
- CONN-01
|
||||||
|
- CONN-02
|
||||||
|
- CONN-03
|
||||||
|
- CLOUD-02
|
||||||
|
- CLOUD-03
|
||||||
|
- CLOUD-04
|
||||||
|
- CLOUD-05
|
||||||
|
- CLOUD-06
|
||||||
|
- CLOUD-07
|
||||||
|
- CLOUD-09
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "Phase 13 backend work starts from failing contracts instead of inferred behavior."
|
||||||
|
- "All four providers are held to one mutation, reconnect, health, and secrecy contract before implementation."
|
||||||
|
- "Reconnect, refreshed-credential persistence, conflict typing, unsupported-operation disclosure, and metadata-only auditing are blocked by tests, not left implicit."
|
||||||
|
artifacts:
|
||||||
|
- path: "backend/tests/test_cloud_mutations.py"
|
||||||
|
provides: "Red endpoint and mutation-result coverage for open, preview, upload, create, rename, move, and delete."
|
||||||
|
- path: "backend/tests/test_cloud_reconnect.py"
|
||||||
|
provides: "Red reconnect, health, cache invalidation, and credential-refresh persistence coverage."
|
||||||
|
- path: "backend/tests/test_cloud_audit.py"
|
||||||
|
provides: "Red metadata-only audit assertions for successful cloud operations."
|
||||||
|
- path: "backend/tests/test_cloud_backends.py"
|
||||||
|
provides: "Provider-specific mutation and conflict/error normalization coverage."
|
||||||
|
- path: "backend/tests/test_cloud_provider_contract.py"
|
||||||
|
provides: "Canonical mutable-adapter contract coverage across Google Drive, OneDrive, Nextcloud, and WebDAV."
|
||||||
|
key_links:
|
||||||
|
- from: "mutable adapter methods"
|
||||||
|
to: "provider contract suites"
|
||||||
|
via: "normalized kind/reason result assertions"
|
||||||
|
pattern: "kind"
|
||||||
|
- from: "OneDrive token refresh success"
|
||||||
|
to: "cloud_connections.credentials_enc persistence"
|
||||||
|
via: "reconnect and content-operation regressions"
|
||||||
|
pattern: "refresh_token"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Create the missing red backend and provider-contract suites for Phase 13 so reconnect, content access, uploads, folder mutations, audit secrecy, and provider normalization are fully specified before implementation.
|
||||||
|
|
||||||
|
Purpose: Turn the resolved Phase 13 decisions into executable backend contracts.
|
||||||
|
Output: Failing backend test files and extensions that cover all ten Phase 13 requirements.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@$HOME/.codex/gsd-core/workflows/execute-plan.md
|
||||||
|
@$HOME/.codex/gsd-core/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@AGENTS.md
|
||||||
|
@.planning/ROADMAP.md
|
||||||
|
@.planning/REQUIREMENTS.md
|
||||||
|
@.planning/phases/13-virtual-local-cloud-operations/13-CONTEXT.md
|
||||||
|
@.planning/phases/13-virtual-local-cloud-operations/13-RESEARCH.md
|
||||||
|
@.planning/phases/13-virtual-local-cloud-operations/13-PATTERNS.md
|
||||||
|
@backend/tests/test_cloud.py
|
||||||
|
@backend/tests/test_cloud_security.py
|
||||||
|
@backend/tests/test_cloud_backends.py
|
||||||
|
@backend/tests/test_cloud_provider_contract.py
|
||||||
|
@backend/tests/test_cloud_items.py
|
||||||
|
</context>
|
||||||
|
|
||||||
|
## Artifacts this phase produces
|
||||||
|
|
||||||
|
- `backend/tests/test_cloud_mutations.py` — red endpoint contracts for typed `{kind, reason}` content and mutation results.
|
||||||
|
- `backend/tests/test_cloud_reconnect.py` — red reconnect, health, cache invalidation, and refreshed-credential persistence coverage.
|
||||||
|
- `backend/tests/test_cloud_audit.py` — red metadata-only audit coverage for successful cloud operations.
|
||||||
|
- Extended `backend/tests/test_cloud_backends.py` and `backend/tests/test_cloud_provider_contract.py` for four-provider mutable-operation coverage.
|
||||||
|
|
||||||
|
## Pattern analogs
|
||||||
|
|
||||||
|
- `backend/tests/test_cloud.py` — connection-ID API integration style and DB side-effect assertions.
|
||||||
|
- `backend/tests/test_cloud_security.py` — owner/admin/credential/SSRF negative coverage style.
|
||||||
|
- `backend/tests/test_cloud_backends.py` — provider-specific fixture and normalization style.
|
||||||
|
- `backend/tests/test_cloud_provider_contract.py` — canonical contract verification style for all providers.
|
||||||
|
- `backend/tests/test_cloud_items.py` — reconciliation and freshness truth assertions.
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Add red API and audit contracts for reconnect, content, and mutation flows</name>
|
||||||
|
<files>backend/tests/test_cloud_mutations.py, backend/tests/test_cloud_reconnect.py, backend/tests/test_cloud_audit.py</files>
|
||||||
|
<read_first>
|
||||||
|
- backend/tests/test_cloud.py
|
||||||
|
- backend/tests/test_cloud_security.py
|
||||||
|
- backend/tests/test_cloud_items.py
|
||||||
|
- .planning/phases/13-virtual-local-cloud-operations/13-CONTEXT.md
|
||||||
|
</read_first>
|
||||||
|
<action>
|
||||||
|
Create failing integration and security tests for D-02 through D-18 and CONN-01 through CONN-03. Require connection-ID reconnect patching of the existing row, explicit connection test behavior, automatic health re-evaluation after credential failures, cached metadata preservation during transient outages, and typed `kind` plus `reason` bodies for conflict, stale, offline, reauth-required, invalid-destination, unsupported-preview, and unsupported-operation outcomes.
|
||||||
|
|
||||||
|
Cover binary-only preview per D-18, authorized download fallback for unsupported preview formats per D-02, metadata-only audit rows for successful reconnect/open/preview/upload/create/rename/move/delete, and disconnect semantics that remove credentials and connection-scoped metadata without touching provider files. Add negative cases proving raw provider URLs, access tokens, refresh tokens, `credentials_enc`, and provider-owned bytes never leak through responses, logs, or audit payloads.
|
||||||
|
</action>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- the new suites fail against the current codebase because the Phase 13 routes and semantics do not yet exist
|
||||||
|
- reconnect tests require patch-in-place row preservation and persisted refreshed credentials
|
||||||
|
- mutation tests require typed `kind` and `reason` bodies instead of Vue-side inference
|
||||||
|
- audit tests reject provider URLs, tokens, bytes, and document content in successful-operation metadata
|
||||||
|
</acceptance_criteria>
|
||||||
|
<verify>
|
||||||
|
<automated>docker compose run --rm backend pytest -v tests/test_cloud_mutations.py tests/test_cloud_reconnect.py tests/test_cloud_audit.py -x</automated>
|
||||||
|
</verify>
|
||||||
|
<done>Backend red suites now define the Phase 13 reconnect, content, mutation, and audit truth.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Extend provider contract suites for four-provider mutable-operation parity</name>
|
||||||
|
<files>backend/tests/test_cloud_backends.py, backend/tests/test_cloud_provider_contract.py</files>
|
||||||
|
<read_first>
|
||||||
|
- backend/tests/test_cloud_backends.py
|
||||||
|
- backend/tests/test_cloud_provider_contract.py
|
||||||
|
- backend/storage/cloud_base.py
|
||||||
|
- backend/storage/google_drive_backend.py
|
||||||
|
- backend/storage/onedrive_backend.py
|
||||||
|
- backend/storage/nextcloud_backend.py
|
||||||
|
- backend/storage/webdav_backend.py
|
||||||
|
- .planning/phases/13-virtual-local-cloud-operations/13-RESEARCH.md
|
||||||
|
</read_first>
|
||||||
|
<action>
|
||||||
|
Add red provider-level coverage for Google Drive, OneDrive, Nextcloud, and WebDAV so the mutable contract explicitly proves normalized create-folder, rename, move, delete, upload, preview-support, and explicit unsupported-operation disclosure behavior. Include D-17 broader Google Drive access expectations, OneDrive refreshed-credential handoff to the service layer, Nextcloud and WebDAV SSRF or redirect protections, collision normalization, trash-versus-permanent delete disclosure, and explicit unsupported preview or replace semantics where providers cannot honor a requested operation.
|
||||||
|
|
||||||
|
Keep the contract provider-neutral: signature, caller identity, no direct `cloud_items` writes, no browse-time byte transfer, and no hidden overwrite path. Use the existing contract-test idiom so later implementation must satisfy one canonical interface instead of per-provider router branching.
|
||||||
|
</action>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- the provider suites fail until mutable contract methods and normalized result types exist
|
||||||
|
- every supported provider has explicit assertions for conflict/error normalization, refreshed-credential persistence handoff, SSRF defense, and unsupported-operation disclosure
|
||||||
|
- no new test encodes provider-specific API payloads in router-visible shapes
|
||||||
|
</acceptance_criteria>
|
||||||
|
<verify>
|
||||||
|
<automated>docker compose run --rm backend pytest -v tests/test_cloud_backends.py tests/test_cloud_provider_contract.py -x</automated>
|
||||||
|
</verify>
|
||||||
|
<done>Four-provider mutable-operation parity is now blocked by red backend contract coverage.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<threat_model>
|
||||||
|
## Trust Boundaries
|
||||||
|
|
||||||
|
| Boundary | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| client → cloud API | Untrusted user input can trigger reconnect, content, and mutation operations. |
|
||||||
|
| cloud API → provider SDK/HTTP | Provider failures and URLs must be normalized before reaching API responses. |
|
||||||
|
| request transaction → audit log | Successful operations must log metadata only, with no secret or byte leakage. |
|
||||||
|
|
||||||
|
## STRIDE Threat Register
|
||||||
|
|
||||||
|
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||||
|
|-----------|----------|-----------|-------------|-----------------|
|
||||||
|
| T-13-01 | E | reconnect/content/mutation routes | mitigate | Red owner-scope tests cover wrong-user, admin, and cross-connection access paths. |
|
||||||
|
| T-13-02 | I | content and preview responses | mitigate | Red tests inspect bodies, headers, and audit rows for provider URLs, tokens, and `credentials_enc`. |
|
||||||
|
| T-13-03 | T | provider conflict handling | mitigate | Red tests require typed `kind`/`reason` conflict bodies and no silent overwrite path. |
|
||||||
|
| T-13-04 | T | Nextcloud/WebDAV outbound calls | mitigate | Provider contract tests require SSRF validation and controlled redirect handling. |
|
||||||
|
| T-13-05 | R | audit trail accuracy | mitigate | Audit tests require metadata-only rows for successful operations and no false overwrite events. |
|
||||||
|
</threat_model>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- Confirm the new backend suites fail in the expected places.
|
||||||
|
- Confirm every backend verification command uses `docker compose run --rm backend pytest ...`.
|
||||||
|
- Confirm provider-level mutable-operation coverage exists for Google Drive, OneDrive, Nextcloud, and WebDAV.
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- Phase 13 backend implementation cannot start without failing tests for reconnect, content, upload, folder mutations, and audit secrecy.
|
||||||
|
- Provider-level contract coverage now includes lifecycle, content, upload, token persistence, unsupported-capability disclosure, conflict normalization, and SSRF protections.
|
||||||
|
- No host `pytest` command remains in this plan.
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
Create `.planning/phases/13-virtual-local-cloud-operations/13-01-SUMMARY.md` when done
|
||||||
|
</output>
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
---
|
||||||
|
phase: "13"
|
||||||
|
plan: "02"
|
||||||
|
type: tdd
|
||||||
|
wave: 0
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- frontend/src/components/storage/__tests__/StorageBrowser.cloud-queue.test.js
|
||||||
|
- frontend/src/views/__tests__/CloudFolderOpenPreview.test.js
|
||||||
|
- frontend/src/components/settings/__tests__/SettingsCloudTab.health.test.js
|
||||||
|
- frontend/src/stores/__tests__/cloudConnections.test.js
|
||||||
|
- frontend/src/views/__tests__/CloudFolderView.test.js
|
||||||
|
- frontend/src/views/__tests__/CloudFolderRenderedFlow.test.js
|
||||||
|
autonomous: true
|
||||||
|
requirements:
|
||||||
|
- CONN-01
|
||||||
|
- CONN-02
|
||||||
|
- CONN-03
|
||||||
|
- CLOUD-02
|
||||||
|
- CLOUD-03
|
||||||
|
- CLOUD-04
|
||||||
|
- CLOUD-05
|
||||||
|
- CLOUD-06
|
||||||
|
- CLOUD-07
|
||||||
|
- CLOUD-09
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "Phase 13 frontend work starts from failing shared-browser and health-state regressions instead of ad hoc UI behavior."
|
||||||
|
- "Queue pause or resume, binary-only preview fallback, reconnect health, and no-probe-on-navigation are all specified before implementation."
|
||||||
|
- "Browser-adjacent health and Settings diagnostics stay aligned because the same red tests cover both surfaces."
|
||||||
|
artifacts:
|
||||||
|
- path: "frontend/src/components/storage/__tests__/StorageBrowser.cloud-queue.test.js"
|
||||||
|
provides: "Red sequential queue and mutation-dialog coverage in the shared browser."
|
||||||
|
- path: "frontend/src/views/__tests__/CloudFolderOpenPreview.test.js"
|
||||||
|
provides: "Red authorized preview and download fallback coverage."
|
||||||
|
- path: "frontend/src/components/settings/__tests__/SettingsCloudTab.health.test.js"
|
||||||
|
provides: "Red Test/Reconnect/Disconnect/consent-copy coverage in Settings."
|
||||||
|
- path: "frontend/src/stores/__tests__/cloudConnections.test.js"
|
||||||
|
provides: "Red health-state translation, auto-test, and no-probe-on-navigation coverage."
|
||||||
|
key_links:
|
||||||
|
- from: "queue conflict and error bodies"
|
||||||
|
to: "StorageBrowser pause and resume UI"
|
||||||
|
via: "shared-browser red tests"
|
||||||
|
pattern: "paused_conflict"
|
||||||
|
- from: "credential failure responses"
|
||||||
|
to: "browser and Settings health state"
|
||||||
|
via: "store and rendered-flow red tests"
|
||||||
|
pattern: "requires_reauth"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Create the missing red frontend and store suites for shared cloud queue behavior, authorized preview, actionable health UX, broader Google Drive consent copy, and the no-probe-on-navigation invariant.
|
||||||
|
|
||||||
|
Purpose: Lock the shared browser and health UX before backend behavior is wired through it.
|
||||||
|
Output: Failing frontend tests that define the only acceptable Phase 13 UI and store behavior.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@$HOME/.codex/gsd-core/workflows/execute-plan.md
|
||||||
|
@$HOME/.codex/gsd-core/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@AGENTS.md
|
||||||
|
@.planning/ROADMAP.md
|
||||||
|
@.planning/phases/13-virtual-local-cloud-operations/13-CONTEXT.md
|
||||||
|
@.planning/phases/13-virtual-local-cloud-operations/13-RESEARCH.md
|
||||||
|
@.planning/phases/13-virtual-local-cloud-operations/13-PATTERNS.md
|
||||||
|
@frontend/src/components/storage/__tests__/StorageBrowser.capabilities.test.js
|
||||||
|
@frontend/src/views/__tests__/CloudFolderView.test.js
|
||||||
|
@frontend/src/views/__tests__/CloudFolderRenderedFlow.test.js
|
||||||
|
@frontend/src/components/settings/__tests__/SettingsCloudTab.test.js
|
||||||
|
@frontend/src/stores/__tests__/cloudConnections.test.js
|
||||||
|
</context>
|
||||||
|
|
||||||
|
## Artifacts this phase produces
|
||||||
|
|
||||||
|
- `frontend/src/components/storage/__tests__/StorageBrowser.cloud-queue.test.js` — red shared queue and mutation-dialog coverage.
|
||||||
|
- `frontend/src/views/__tests__/CloudFolderOpenPreview.test.js` — red authorized preview and download fallback coverage.
|
||||||
|
- `frontend/src/components/settings/__tests__/SettingsCloudTab.health.test.js` — red health, reconnect, disconnect, and broader-scope consent coverage.
|
||||||
|
- Extended `frontend/src/stores/__tests__/cloudConnections.test.js`, `frontend/src/views/__tests__/CloudFolderView.test.js`, and `frontend/src/views/__tests__/CloudFolderRenderedFlow.test.js` for no-probe and health-state regressions.
|
||||||
|
|
||||||
|
## Pattern analogs
|
||||||
|
|
||||||
|
- `frontend/src/components/storage/__tests__/StorageBrowser.capabilities.test.js` — emitted-event and capability UI assertions.
|
||||||
|
- `frontend/src/views/__tests__/CloudFolderView.test.js` — thin-view orchestration style.
|
||||||
|
- `frontend/src/views/__tests__/CloudFolderRenderedFlow.test.js` — rendered shared-browser flow style.
|
||||||
|
- `frontend/src/components/settings/__tests__/SettingsCloudTab.test.js` — settings action and confirmation style.
|
||||||
|
- `frontend/src/stores/__tests__/cloudConnections.test.js` — store mapping and reset-behavior style.
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Add red shared-browser tests for queue, preview, and fallback download behavior</name>
|
||||||
|
<files>frontend/src/components/storage/__tests__/StorageBrowser.cloud-queue.test.js, frontend/src/views/__tests__/CloudFolderOpenPreview.test.js, frontend/src/views/__tests__/CloudFolderView.test.js</files>
|
||||||
|
<read_first>
|
||||||
|
- frontend/src/components/storage/__tests__/StorageBrowser.capabilities.test.js
|
||||||
|
- frontend/src/views/__tests__/CloudFolderView.test.js
|
||||||
|
- frontend/src/views/__tests__/CloudFolderRenderedFlow.test.js
|
||||||
|
- .planning/phases/13-virtual-local-cloud-operations/13-CONTEXT.md
|
||||||
|
</read_first>
|
||||||
|
<action>
|
||||||
|
Create failing tests that require D-01 through D-04 and D-18 exactly: cloud upload runs as a sequential queue, pauses the whole queue on a typed conflict or error body, preserves remaining items, and resumes only after Keep both, Replace, Skip, Retry, or Cancel all. Add open and preview tests that require binary-only in-app preview, ownership-checked download fallback for unsupported formats, and zero `window.open()` or raw provider URL usage. Extend the thin-view suite so CloudFolderView must remain a data provider over `StorageBrowser`, not a parallel cloud grid.
|
||||||
|
</action>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- the new queue and preview tests fail against the current placeholder cloud handlers
|
||||||
|
- red tests require backend-authored conflict/error typing instead of Vue-side guessing
|
||||||
|
- unsupported preview formats are required to fall back to authorized download rather than Office-native or Google Workspace preview
|
||||||
|
</acceptance_criteria>
|
||||||
|
<verify>
|
||||||
|
<automated>cd frontend && npm run test -- --run src/components/storage/__tests__/StorageBrowser.cloud-queue.test.js src/views/__tests__/CloudFolderOpenPreview.test.js src/views/__tests__/CloudFolderView.test.js</automated>
|
||||||
|
</verify>
|
||||||
|
<done>Frontend red tests now define the only acceptable shared queue and preview behavior.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Add red store and health-flow tests for reconnect, broader Google consent, and no navigation probe</name>
|
||||||
|
<files>frontend/src/components/settings/__tests__/SettingsCloudTab.health.test.js, frontend/src/stores/__tests__/cloudConnections.test.js, frontend/src/views/__tests__/CloudFolderRenderedFlow.test.js</files>
|
||||||
|
<read_first>
|
||||||
|
- frontend/src/components/settings/__tests__/SettingsCloudTab.test.js
|
||||||
|
- frontend/src/stores/__tests__/cloudConnections.test.js
|
||||||
|
- frontend/src/views/__tests__/CloudFolderRenderedFlow.test.js
|
||||||
|
- .planning/phases/13-virtual-local-cloud-operations/13-RESEARCH.md
|
||||||
|
- .planning/phases/13-virtual-local-cloud-operations/13-CONTEXT.md
|
||||||
|
</read_first>
|
||||||
|
<action>
|
||||||
|
Add failing tests for D-12 through D-17: compact actionable health beside the cloud browser, fuller diagnostics plus explicit Test and Reconnect controls in Settings, automatic health retest after connect, reconnect, and credential-related failures, preserved stale metadata during transient outages, and an explicit invariant that ordinary folder navigation never triggers a provider health probe. Make the Google Drive consent copy explicit about broader storage access so the expanded Phase 13 scope cannot ship silently.
|
||||||
|
|
||||||
|
Keep the behavior store-led: the store must translate server health states once for both browser and Settings, record when reconnect should refresh the current folder, and distinguish transient offline state from reauthentication. The rendered-flow test should confirm the browser keeps cached items visible while warning state and reconnect actions are shown.
|
||||||
|
</action>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- health-flow tests fail unless automatic post-failure retest and no-probe-on-navigation behavior are implemented
|
||||||
|
- the settings suite fails unless broader Google Drive access is explained in the user-facing consent/reconnect copy
|
||||||
|
- rendered-flow tests fail unless stale metadata remains visible during warning and reconnect states
|
||||||
|
</acceptance_criteria>
|
||||||
|
<verify>
|
||||||
|
<automated>cd frontend && npm run test -- --run src/components/settings/__tests__/SettingsCloudTab.health.test.js src/stores/__tests__/cloudConnections.test.js src/views/__tests__/CloudFolderRenderedFlow.test.js</automated>
|
||||||
|
</verify>
|
||||||
|
<done>Frontend red tests now lock health-state, reconnect, consent, and no-probe behavior before implementation.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<threat_model>
|
||||||
|
## Trust Boundaries
|
||||||
|
|
||||||
|
| Boundary | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| server health state → store/UI | Trusted backend status must not be replaced by client-side guesswork. |
|
||||||
|
| user interaction → shared browser dialogs | Conflict and delete choices must remain explicit and auditable. |
|
||||||
|
|
||||||
|
## STRIDE Threat Register
|
||||||
|
|
||||||
|
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||||
|
|-----------|----------|-----------|-------------|-----------------|
|
||||||
|
| T-13-06 | T | cloud health UI | mitigate | Red tests require no background probe on ordinary navigation and explicit server-driven health mapping. |
|
||||||
|
| T-13-07 | I | preview and download UI | mitigate | Red tests forbid raw provider URLs and require authorized preview/download helpers only. |
|
||||||
|
| T-13-08 | R | queue conflict decisions | mitigate | Red tests require explicit pause/resume choices and no silent replace or hidden cancel path. |
|
||||||
|
| T-13-09 | S | reconnect/consent UX | mitigate | Red settings tests require explicit broader Google scope copy and reconnect affordances. |
|
||||||
|
</threat_model>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- Confirm the queue, preview, health, store, and rendered-flow suites fail in the expected places.
|
||||||
|
- Confirm no frontend test assumes cloud-only layout or browser-direct provider URLs.
|
||||||
|
- Confirm the no-probe-on-navigation invariant is explicitly asserted.
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- Phase 13 frontend implementation is blocked by failing shared-browser, store, and Settings regressions.
|
||||||
|
- Broader Google Drive access, binary-only preview, and health re-evaluation rules are concretely encoded.
|
||||||
|
- No backend host-venv assumption appears anywhere in this plan.
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
Create `.planning/phases/13-virtual-local-cloud-operations/13-02-SUMMARY.md` when done
|
||||||
|
</output>
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
---
|
||||||
|
phase: "13"
|
||||||
|
plan: "03"
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on:
|
||||||
|
- "13-01"
|
||||||
|
files_modified:
|
||||||
|
- backend/storage/cloud_base.py
|
||||||
|
- backend/storage/cloud_backend_factory.py
|
||||||
|
- backend/services/cloud_operations.py
|
||||||
|
- backend/services/cloud_items.py
|
||||||
|
- backend/tests/test_cloud_backends.py
|
||||||
|
- backend/tests/test_cloud_provider_contract.py
|
||||||
|
- backend/tests/test_cloud_reconnect.py
|
||||||
|
autonomous: true
|
||||||
|
requirements:
|
||||||
|
- CONN-01
|
||||||
|
- CONN-02
|
||||||
|
- CONN-03
|
||||||
|
- CLOUD-02
|
||||||
|
- CLOUD-03
|
||||||
|
- CLOUD-04
|
||||||
|
- CLOUD-05
|
||||||
|
- CLOUD-06
|
||||||
|
- CLOUD-07
|
||||||
|
- CLOUD-09
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "Provider-neutral mutable-operation contracts exist before routes or UI are wired."
|
||||||
|
- "Providers can hand refreshed credentials upward without writing the database themselves."
|
||||||
|
- "Reconciliation and freshness remain centralized in `cloud_items.py` even for mutable work."
|
||||||
|
artifacts:
|
||||||
|
- path: "backend/storage/cloud_base.py"
|
||||||
|
provides: "Normalized mutable-operation result types, health states, and provider error vocabulary."
|
||||||
|
- path: "backend/services/cloud_operations.py"
|
||||||
|
provides: "Owner-scoped orchestration for reconnect, content, upload, and folder mutation work."
|
||||||
|
key_links:
|
||||||
|
- from: "provider refresh outcomes"
|
||||||
|
to: "encrypted credential persistence"
|
||||||
|
via: "cloud_operations orchestration"
|
||||||
|
pattern: "credentials"
|
||||||
|
- from: "mutable provider results"
|
||||||
|
to: "cloud_items reconciliation"
|
||||||
|
via: "single service-layer integration point"
|
||||||
|
pattern: "reconcile"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Build the Phase 13 backend foundation: the mutable cloud contract, provider-factory assertions, and the service-layer orchestration seam that owns credential refresh handoff, stale classification, and centralized reconciliation.
|
||||||
|
|
||||||
|
Purpose: Create the backend contract layer every later Phase 13 route and provider change depends on.
|
||||||
|
Output: A provider-neutral mutable adapter interface and a single orchestration module.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@$HOME/.codex/gsd-core/workflows/execute-plan.md
|
||||||
|
@$HOME/.codex/gsd-core/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@AGENTS.md
|
||||||
|
@.planning/phases/13-virtual-local-cloud-operations/13-CONTEXT.md
|
||||||
|
@.planning/phases/13-virtual-local-cloud-operations/13-RESEARCH.md
|
||||||
|
@.planning/phases/13-virtual-local-cloud-operations/13-PATTERNS.md
|
||||||
|
@backend/storage/cloud_base.py
|
||||||
|
@backend/storage/cloud_backend_factory.py
|
||||||
|
@backend/services/cloud_items.py
|
||||||
|
@backend/tests/test_cloud_backends.py
|
||||||
|
@backend/tests/test_cloud_provider_contract.py
|
||||||
|
@backend/tests/test_cloud_reconnect.py
|
||||||
|
</context>
|
||||||
|
|
||||||
|
## Artifacts this phase produces
|
||||||
|
|
||||||
|
- `backend/storage/cloud_base.py` — mutable-operation dataclasses, health/result enums, and domain exceptions.
|
||||||
|
- `backend/services/cloud_operations.py` — owner-scoped orchestration for reconnect, content, upload, and folder mutations.
|
||||||
|
- `backend/storage/cloud_backend_factory.py` — factory assertions for the mutable contract.
|
||||||
|
|
||||||
|
## Pattern analogs
|
||||||
|
|
||||||
|
- `backend/services/cloud_items.py` — centralized reconciliation and freshness ownership.
|
||||||
|
- `backend/storage/cloud_backend_factory.py` — provider construction and interface assertion boundary.
|
||||||
|
- `backend/tests/test_cloud_provider_contract.py` — canonical contract-verification style.
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto" tdd="true">
|
||||||
|
<name>Task 1: Extend the shared cloud contract for mutable operations and normalized outcomes</name>
|
||||||
|
<files>backend/storage/cloud_base.py, backend/storage/cloud_backend_factory.py, backend/tests/test_cloud_backends.py, backend/tests/test_cloud_provider_contract.py</files>
|
||||||
|
<read_first>
|
||||||
|
- backend/storage/cloud_base.py
|
||||||
|
- backend/storage/cloud_backend_factory.py
|
||||||
|
- backend/tests/test_cloud_backends.py
|
||||||
|
- backend/tests/test_cloud_provider_contract.py
|
||||||
|
- .planning/phases/13-virtual-local-cloud-operations/13-RESEARCH.md
|
||||||
|
</read_first>
|
||||||
|
<behavior>
|
||||||
|
- Test 1: providers expose one mutable-operation contract with normalized success, conflict, stale, offline, and unsupported results
|
||||||
|
- Test 2: providers can hand refreshed credentials upward without persisting them directly
|
||||||
|
- Test 3: preview support is explicit and binary-only rather than inferred from provider MIME quirks
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
Extend `backend/storage/cloud_base.py` with the mutable-operation interface and normalized result dataclasses for health, reconnect, preview support, upload conflict, create/rename collision, move validation, delete disclosure, and unsupported-operation reporting. Keep `kind` and `reason` vocabulary centralized here so every provider and route shares the same typed contract. Update `backend/storage/cloud_backend_factory.py` to assert the new interface without introducing provider-name switches in routers or services.
|
||||||
|
</action>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- the provider contract suites from 13-01 pass for normalized signatures and result shapes
|
||||||
|
- binary-only preview support is explicit in the shared contract
|
||||||
|
- providers are not given permission to write audit rows or `cloud_items` directly
|
||||||
|
</acceptance_criteria>
|
||||||
|
<verify>
|
||||||
|
<automated>docker compose run --rm backend pytest -v tests/test_cloud_backends.py tests/test_cloud_provider_contract.py -x</automated>
|
||||||
|
</verify>
|
||||||
|
<done>The codebase now has one canonical mutable cloud contract for all later Phase 13 backend work.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto" tdd="true">
|
||||||
|
<name>Task 2: Create the cloud operations orchestration seam without breaking freshness ownership</name>
|
||||||
|
<files>backend/services/cloud_operations.py, backend/services/cloud_items.py, backend/tests/test_cloud_reconnect.py</files>
|
||||||
|
<read_first>
|
||||||
|
- backend/services/cloud_items.py
|
||||||
|
- backend/tests/test_cloud_reconnect.py
|
||||||
|
- backend/services/audit.py
|
||||||
|
- .planning/phases/13-virtual-local-cloud-operations/13-CONTEXT.md
|
||||||
|
</read_first>
|
||||||
|
<behavior>
|
||||||
|
- Test 1: the orchestration layer resolves owned connections and classifies reauth versus transient offline states
|
||||||
|
- Test 2: refreshed credentials persist only above the provider boundary
|
||||||
|
- Test 3: reconnect and mutation work invalidate caches and route back through centralized reconciliation rather than direct ORM writes
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
Create `backend/services/cloud_operations.py` as the single service-layer seam for D-13 through D-16. It must resolve the owned connection, decrypt credentials only at the provider boundary, accept refreshed credentials back from providers for encrypted persistence, classify credential failures versus transient outages, and funnel listing invalidation or follow-up refresh through `cloud_items.py`. Keep service exceptions domain-specific or `ValueError` only; routers remain responsible for translating them to `HTTPException`.
|
||||||
|
</action>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- reconnect red tests pass at the orchestration layer
|
||||||
|
- persisted refreshed credentials are handled only in the service layer, never inside providers
|
||||||
|
- `cloud_items.py` remains the sole reconciliation and freshness authority
|
||||||
|
</acceptance_criteria>
|
||||||
|
<verify>
|
||||||
|
<automated>docker compose run --rm backend pytest -v tests/test_cloud_reconnect.py tests/test_cloud_provider_contract.py -x</automated>
|
||||||
|
</verify>
|
||||||
|
<done>The backend now has a single orchestration seam that later routes and providers can safely build on.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<threat_model>
|
||||||
|
## Trust Boundaries
|
||||||
|
|
||||||
|
| Boundary | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| router/service → provider | Provider exceptions and refreshed credentials must be normalized before leaving the backend boundary. |
|
||||||
|
| service → database | Only orchestration code may persist refreshed credentials or trigger reconciliation. |
|
||||||
|
|
||||||
|
## STRIDE Threat Register
|
||||||
|
|
||||||
|
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||||
|
|-----------|----------|-----------|-------------|-----------------|
|
||||||
|
| T-13-10 | T | mutable contract layer | mitigate | Centralize `kind`/`reason` result vocabulary in `cloud_base.py` and verify with provider contract tests. |
|
||||||
|
| T-13-11 | I | refreshed credentials | mitigate | Persist only encrypted credentials above the provider boundary; verify in reconnect tests. |
|
||||||
|
| T-13-12 | T | reconciliation path | mitigate | Route all mutable follow-up work through `cloud_items.py`; forbid direct provider ORM writes. |
|
||||||
|
</threat_model>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- Pass the provider contract and reconnect suites introduced in Wave 0.
|
||||||
|
- Confirm every backend verify command remains containerized.
|
||||||
|
- Confirm the orchestration seam is the only place where refreshed credentials can be persisted.
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- Mutable-operation contracts and the orchestration seam exist before route work begins.
|
||||||
|
- Provider-level contract coverage now has a real shared interface to validate.
|
||||||
|
- Freshness and reconciliation ownership remain centralized instead of fragmenting across routers or adapters.
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
Create `.planning/phases/13-virtual-local-cloud-operations/13-03-SUMMARY.md` when done
|
||||||
|
</output>
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
---
|
||||||
|
phase: "13"
|
||||||
|
plan: "04"
|
||||||
|
type: execute
|
||||||
|
wave: 2
|
||||||
|
depends_on:
|
||||||
|
- "13-01"
|
||||||
|
- "13-03"
|
||||||
|
files_modified:
|
||||||
|
- backend/api/cloud/operations.py
|
||||||
|
- backend/api/cloud/connections.py
|
||||||
|
- backend/api/cloud/schemas.py
|
||||||
|
- frontend/src/api/cloud.js
|
||||||
|
- backend/tests/test_cloud_mutations.py
|
||||||
|
- backend/tests/test_cloud_reconnect.py
|
||||||
|
- backend/tests/test_cloud_security.py
|
||||||
|
autonomous: true
|
||||||
|
requirements:
|
||||||
|
- CONN-01
|
||||||
|
- CONN-02
|
||||||
|
- CONN-03
|
||||||
|
- CLOUD-02
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "Reconnect is a connection-ID patch flow, not a second-row account insertion."
|
||||||
|
- "Google Drive reconnect and connect flows explicitly request broader Phase 13 access."
|
||||||
|
- "Open, preview, and fallback download use DocuVault-controlled endpoints with typed result bodies and binary-only preview rules."
|
||||||
|
artifacts:
|
||||||
|
- path: "backend/api/cloud/operations.py"
|
||||||
|
provides: "Owner-scoped test, open, preview, and fallback download endpoints."
|
||||||
|
- path: "backend/api/cloud/connections.py"
|
||||||
|
provides: "Connection-ID reconnect patching and broader Drive OAuth scope handling."
|
||||||
|
- path: "backend/api/cloud/schemas.py"
|
||||||
|
provides: "Whitelisted typed health, reconnect, and content result schemas."
|
||||||
|
key_links:
|
||||||
|
- from: "OAuth reconnect state"
|
||||||
|
to: "existing cloud_connections row"
|
||||||
|
via: "connection-ID patching"
|
||||||
|
pattern: "connection_id"
|
||||||
|
- from: "preview result typing"
|
||||||
|
to: "frontend shared browser handlers"
|
||||||
|
via: "whitelisted kind/reason response bodies"
|
||||||
|
pattern: "reason"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Implement the connection-ID reconnect, explicit health-test, and authorized content-route slice of Phase 13, including broader Google Drive scope handling and binary-only preview fallback rules.
|
||||||
|
|
||||||
|
Purpose: Put the owner-scoped route layer in place before upload and folder mutations build on it.
|
||||||
|
Output: Typed reconnect and content endpoints plus centralized frontend API helpers.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@$HOME/.codex/gsd-core/workflows/execute-plan.md
|
||||||
|
@$HOME/.codex/gsd-core/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@AGENTS.md
|
||||||
|
@.planning/phases/13-virtual-local-cloud-operations/13-CONTEXT.md
|
||||||
|
@.planning/phases/13-virtual-local-cloud-operations/13-RESEARCH.md
|
||||||
|
@.planning/phases/13-virtual-local-cloud-operations/13-PATTERNS.md
|
||||||
|
@backend/api/cloud/connections.py
|
||||||
|
@backend/api/cloud/schemas.py
|
||||||
|
@frontend/src/api/cloud.js
|
||||||
|
@backend/tests/test_cloud_mutations.py
|
||||||
|
@backend/tests/test_cloud_reconnect.py
|
||||||
|
@backend/tests/test_cloud_security.py
|
||||||
|
</context>
|
||||||
|
|
||||||
|
## Artifacts this phase produces
|
||||||
|
|
||||||
|
- `backend/api/cloud/operations.py` — owner-scoped explicit test and content-access routes.
|
||||||
|
- `backend/api/cloud/connections.py` — connection-ID reconnect patching and scope-aware OAuth handling.
|
||||||
|
- `backend/api/cloud/schemas.py` — typed health, reconnect, preview, and download fallback bodies.
|
||||||
|
- `frontend/src/api/cloud.js` — centralized client helpers for the new route family.
|
||||||
|
|
||||||
|
## Pattern analogs
|
||||||
|
|
||||||
|
- `backend/api/cloud/browse.py` — owner-scoped connection-ID route structure.
|
||||||
|
- `backend/api/documents/content.py` — authorized streaming and controlled error translation.
|
||||||
|
- `backend/api/cloud/connections.py` — existing connect/update/disconnect patterns.
|
||||||
|
- `frontend/src/api/cloud.js` — centralized client boundary for cloud routes.
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto" tdd="true">
|
||||||
|
<name>Task 1: Patch reconnect and explicit test flows onto the existing connection row</name>
|
||||||
|
<files>backend/api/cloud/connections.py, backend/api/cloud/schemas.py, frontend/src/api/cloud.js, backend/tests/test_cloud_reconnect.py, backend/tests/test_cloud_security.py</files>
|
||||||
|
<read_first>
|
||||||
|
- backend/api/cloud/connections.py
|
||||||
|
- backend/api/cloud/schemas.py
|
||||||
|
- frontend/src/api/cloud.js
|
||||||
|
- backend/tests/test_cloud_reconnect.py
|
||||||
|
- .planning/phases/13-virtual-local-cloud-operations/13-RESEARCH.md
|
||||||
|
</read_first>
|
||||||
|
<behavior>
|
||||||
|
- Test 1: reconnect patches the existing owned connection row by ID and invalidates caches without deleting metadata
|
||||||
|
- Test 2: explicit test and credential-failure recovery classify reauth versus transient offline states
|
||||||
|
- Test 3: Google Drive OAuth uses the broader Phase 13 access scope and exposes that state through controlled responses
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
Update the OAuth initiation and callback flow so reconnect is an existing-connection repair path keyed by connection ID rather than a new-row insertion. Persist refreshed encrypted credentials in place, invalidate connection-scoped capability and listing caches, and mark cached metadata stale while a refresh follows D-14. Preserve D-15 by keeping transient outages non-destructive. Make the Google Drive path request the broader Phase 13 scope explicitly and keep the response schema typed so the frontend can render honest consent and health states without parsing provider payloads.
|
||||||
|
</action>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- reconnect and security tests pass without creating a second connection row
|
||||||
|
- broader Google Drive access is explicit in the backend flow rather than implicit or undocumented
|
||||||
|
- transient offline and reauth-required states are distinguishable and credential-safe
|
||||||
|
</acceptance_criteria>
|
||||||
|
<verify>
|
||||||
|
<automated>docker compose run --rm backend pytest -v tests/test_cloud_reconnect.py tests/test_cloud_security.py -k "reconnect or health or credential or scope" -x</automated>
|
||||||
|
</verify>
|
||||||
|
<done>Reconnect and health routes now satisfy the connection-ID, scope, and cache-invalidating Phase 13 contract.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto" tdd="true">
|
||||||
|
<name>Task 2: Add authorized open, preview, and fallback download routes with typed bodies</name>
|
||||||
|
<files>backend/api/cloud/operations.py, backend/api/cloud/schemas.py, frontend/src/api/cloud.js, backend/tests/test_cloud_mutations.py, backend/tests/test_cloud_security.py</files>
|
||||||
|
<read_first>
|
||||||
|
- backend/api/documents/content.py
|
||||||
|
- backend/api/cloud/schemas.py
|
||||||
|
- backend/tests/test_cloud_mutations.py
|
||||||
|
- backend/tests/test_cloud_security.py
|
||||||
|
- .planning/phases/13-virtual-local-cloud-operations/13-CONTEXT.md
|
||||||
|
</read_first>
|
||||||
|
<behavior>
|
||||||
|
- Test 1: authorized content routes expose binary-only preview and typed unsupported-preview fallback
|
||||||
|
- Test 2: preview and download responses never expose raw provider URLs or credentials
|
||||||
|
- Test 3: ordinary folder navigation still does not trigger a provider health probe
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
Add `backend/api/cloud/operations.py` and expose owner-scoped open, preview, and authorized download fallback routes that call the service layer only. Use typed response and error bodies with stable `kind` and `reason` codes, limit preview support to the Phase 13 binary matrix, and keep unsupported formats on the download fallback path. Extend `frontend/src/api/cloud.js` with centralized helpers for the new routes so later UI work does not call raw URLs or construct provider-specific content paths.
|
||||||
|
</action>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- content and security suites pass with typed route outputs
|
||||||
|
- preview support remains explicitly binary-only and does not introduce Office-native or Google Workspace rendering
|
||||||
|
- no content route leaks provider URLs, tokens, or decrypted credentials
|
||||||
|
</acceptance_criteria>
|
||||||
|
<verify>
|
||||||
|
<automated>docker compose run --rm backend pytest -v tests/test_cloud_mutations.py tests/test_cloud_security.py -k "open or preview or download or content" -x</automated>
|
||||||
|
</verify>
|
||||||
|
<done>The route layer now exposes safe reconnect and content endpoints that later upload and UI plans can reuse.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<threat_model>
|
||||||
|
## Trust Boundaries
|
||||||
|
|
||||||
|
| Boundary | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| browser → reconnect/content routes | Untrusted requests must stay owner-scoped and CSRF-protected. |
|
||||||
|
| cloud route → provider bytes | Provider content must stay behind DocuVault authorization and typed error shaping. |
|
||||||
|
|
||||||
|
## STRIDE Threat Register
|
||||||
|
|
||||||
|
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||||
|
|-----------|----------|-----------|-------------|-----------------|
|
||||||
|
| T-13-13 | E | reconnect flow | mitigate | Connection-ID patching and owner-scoped lookup are verified in reconnect and security tests. |
|
||||||
|
| T-13-14 | I | preview/download routes | mitigate | Typed route outputs plus content/security tests forbid provider URL or token leakage. |
|
||||||
|
| T-13-15 | T | navigation health behavior | mitigate | Reconnect tests and later UI tests assert no health probe on ordinary folder navigation. |
|
||||||
|
</threat_model>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- Pass the reconnect, content, and security suites introduced in Wave 0.
|
||||||
|
- Confirm every backend verification command is containerized.
|
||||||
|
- Confirm binary-only preview and broader Drive scope handling are explicitly encoded in the route layer.
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- Reconnect now patches the existing connection row and exposes honest health states.
|
||||||
|
- The backend has safe open, preview, and fallback download routes with typed result bodies.
|
||||||
|
- Phase 13 now encodes broader Google Drive access, binary-only preview, and no-probe-on-navigation rules concretely.
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
Create `.planning/phases/13-virtual-local-cloud-operations/13-04-SUMMARY.md` when done
|
||||||
|
</output>
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
---
|
||||||
|
phase: "13"
|
||||||
|
plan: "05"
|
||||||
|
type: execute
|
||||||
|
wave: 3
|
||||||
|
depends_on:
|
||||||
|
- "13-01"
|
||||||
|
- "13-03"
|
||||||
|
- "13-04"
|
||||||
|
files_modified:
|
||||||
|
- backend/api/cloud/operations.py
|
||||||
|
- backend/api/cloud/schemas.py
|
||||||
|
- backend/services/cloud_operations.py
|
||||||
|
- backend/storage/google_drive_backend.py
|
||||||
|
- backend/storage/onedrive_backend.py
|
||||||
|
- backend/storage/webdav_backend.py
|
||||||
|
- backend/tests/test_cloud_mutations.py
|
||||||
|
- backend/tests/test_cloud_backends.py
|
||||||
|
- backend/tests/test_cloud_provider_contract.py
|
||||||
|
autonomous: true
|
||||||
|
requirements:
|
||||||
|
- CLOUD-03
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "Cloud upload uses typed conflict and error bodies instead of hidden overwrite semantics."
|
||||||
|
- "Provider upload differences stay behind the mutable adapter and service contract."
|
||||||
|
- "Refreshed provider credentials can be handed upward during upload work without provider-owned database writes."
|
||||||
|
artifacts:
|
||||||
|
- path: "backend/api/cloud/operations.py"
|
||||||
|
provides: "Connection-ID upload endpoint with typed conflict and retry result bodies."
|
||||||
|
- path: "backend/services/cloud_operations.py"
|
||||||
|
provides: "Upload dispatch that normalizes keep-both, replace, retryable error, and unsupported-replace outcomes."
|
||||||
|
key_links:
|
||||||
|
- from: "provider upload result"
|
||||||
|
to: "shared queue resume decisions"
|
||||||
|
via: "typed kind/reason response bodies"
|
||||||
|
pattern: "conflict"
|
||||||
|
- from: "refreshed provider credentials"
|
||||||
|
to: "service-layer persistence handoff"
|
||||||
|
via: "upload success and retry paths"
|
||||||
|
pattern: "credentials"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Implement the bounded backend upload mechanics slice of Phase 13: connection-ID upload routing, typed conflict and retry bodies, provider-normalized keep-both or replace handling, and refreshed-credential handoff without yet folding in reconcile or audit follow-through.
|
||||||
|
|
||||||
|
Purpose: Finish the provider and route mechanics the shared upload queue needs before the authoritative success path is layered on top.
|
||||||
|
Output: Upload-capable provider adapters, route and service dispatch, and passing backend upload mechanics suites.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@$HOME/.codex/gsd-core/workflows/execute-plan.md
|
||||||
|
@$HOME/.codex/gsd-core/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@AGENTS.md
|
||||||
|
@.planning/phases/13-virtual-local-cloud-operations/13-CONTEXT.md
|
||||||
|
@.planning/phases/13-virtual-local-cloud-operations/13-RESEARCH.md
|
||||||
|
@.planning/phases/13-virtual-local-cloud-operations/13-PATTERNS.md
|
||||||
|
@backend/api/cloud/operations.py
|
||||||
|
@backend/services/cloud_operations.py
|
||||||
|
@backend/storage/google_drive_backend.py
|
||||||
|
@backend/storage/onedrive_backend.py
|
||||||
|
@backend/storage/webdav_backend.py
|
||||||
|
</context>
|
||||||
|
|
||||||
|
## Artifacts this phase produces
|
||||||
|
|
||||||
|
- Upload support in `backend/api/cloud/operations.py` and `backend/services/cloud_operations.py`.
|
||||||
|
- Provider-normalized keep-both, replace, retry, skip, and unsupported-replace semantics across Google Drive, OneDrive, Nextcloud-via-WebDAV, and generic WebDAV.
|
||||||
|
- Passing upload and provider-contract suites for the mechanics half of `CLOUD-03`.
|
||||||
|
|
||||||
|
## Pattern analogs
|
||||||
|
|
||||||
|
- `backend/api/documents/upload.py` — multipart upload proxy pattern.
|
||||||
|
- `backend/tests/test_cloud_backends.py` and `backend/tests/test_cloud_provider_contract.py` — four-provider normalization assertions.
|
||||||
|
- `backend/services/cloud_operations.py` from 13-03 — refreshed-credential handoff and typed domain-result pattern.
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto" tdd="true">
|
||||||
|
<name>Task 1: Implement provider-normalized upload conflict semantics across the adapter boundary</name>
|
||||||
|
<files>backend/storage/google_drive_backend.py, backend/storage/onedrive_backend.py, backend/storage/webdav_backend.py, backend/tests/test_cloud_backends.py, backend/tests/test_cloud_provider_contract.py</files>
|
||||||
|
<read_first>
|
||||||
|
- backend/storage/google_drive_backend.py
|
||||||
|
- backend/storage/onedrive_backend.py
|
||||||
|
- backend/storage/webdav_backend.py
|
||||||
|
- backend/tests/test_cloud_backends.py
|
||||||
|
- backend/tests/test_cloud_provider_contract.py
|
||||||
|
- .planning/phases/13-virtual-local-cloud-operations/13-CONTEXT.md
|
||||||
|
</read_first>
|
||||||
|
<behavior>
|
||||||
|
- Test 1: upload can return success, typed conflict, typed retryable error, or explicit unsupported-replace results
|
||||||
|
- Test 2: keep-both naming is authoritative and inserts the counter before the file extension
|
||||||
|
- Test 3: provider-specific conflict semantics normalize into one shared contract across all four providers
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
Implement D-03 and D-04 inside the provider layer so Google Drive, OneDrive, and the shared WebDAV path normalize upload conflicts, replace support, keep-both suffixing, and retryable transient failures into one mutable-operation contract. Preserve broader Google Drive access assumptions, OneDrive refreshed-credential handoff, and SSRF validation on every WebDAV upload request. Keep Nextcloud on the shared WebDAV mutation path unless a narrow override is strictly required.
|
||||||
|
</action>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- upload provider suites pass for all four supported providers
|
||||||
|
- keep-both naming is authoritative and consistent across providers
|
||||||
|
- provider code returns typed results and never writes `cloud_items`, audit rows, or router-visible provider payloads directly
|
||||||
|
</acceptance_criteria>
|
||||||
|
<verify>
|
||||||
|
<automated>docker compose run --rm backend pytest -v tests/test_cloud_backends.py tests/test_cloud_provider_contract.py -k "upload or replace or conflict" -x</automated>
|
||||||
|
</verify>
|
||||||
|
<done>The adapter layer now exposes typed, provider-normalized upload mechanics for the shared queue.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto" tdd="true">
|
||||||
|
<name>Task 2: Expose connection-ID upload dispatch with typed route and service results</name>
|
||||||
|
<files>backend/api/cloud/operations.py, backend/api/cloud/schemas.py, backend/services/cloud_operations.py, backend/tests/test_cloud_mutations.py</files>
|
||||||
|
<read_first>
|
||||||
|
- backend/api/cloud/operations.py
|
||||||
|
- backend/api/cloud/schemas.py
|
||||||
|
- backend/services/cloud_operations.py
|
||||||
|
- backend/tests/test_cloud_mutations.py
|
||||||
|
- .planning/phases/13-virtual-local-cloud-operations/13-CONTEXT.md
|
||||||
|
</read_first>
|
||||||
|
<behavior>
|
||||||
|
- Test 1: the upload route returns typed success, conflict, retryable error, and unsupported-replace bodies
|
||||||
|
- Test 2: the service layer owns refreshed-credential persistence handoff above the provider boundary
|
||||||
|
- Test 3: queue-control semantics come from backend-authored results instead of Vue-side inference
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
Wire the connection-ID upload endpoint through `backend/services/cloud_operations.py` and `backend/api/cloud/schemas.py` so the route returns stable `kind` and `reason` bodies for Keep both, Replace, Skip, Retry, Cancel all, or unsupported-replace outcomes. Keep this plan bounded to mechanics: normalize provider results, surface refreshed credentials upward for encrypted persistence, and leave centralized reconciliation plus metadata-only audit follow-through to the next plan.
|
||||||
|
</action>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- upload route and mutation suites pass for typed mechanics and credential-safe results
|
||||||
|
- queue-control semantics are backend-authored and credential-safe
|
||||||
|
- this plan does not yet add upload-specific reconciliation or success-audit behavior outside the existing foundations
|
||||||
|
</acceptance_criteria>
|
||||||
|
<verify>
|
||||||
|
<automated>docker compose run --rm backend pytest -v tests/test_cloud_mutations.py tests/test_cloud_backends.py tests/test_cloud_provider_contract.py -k "upload or replace or conflict" -x</automated>
|
||||||
|
</verify>
|
||||||
|
<done>The backend now exposes the bounded upload mechanics the next follow-through plan can finish.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<threat_model>
|
||||||
|
## Trust Boundaries
|
||||||
|
|
||||||
|
| Boundary | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| browser upload → provider write | User content enters a provider-owned mutation path through DocuVault authorization only. |
|
||||||
|
| provider SDK/HTTP → route result | Provider conflict, retry, and replace behavior must be normalized before Vue consumes it. |
|
||||||
|
|
||||||
|
## STRIDE Threat Register
|
||||||
|
|
||||||
|
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||||
|
|-----------|----------|-----------|-------------|-----------------|
|
||||||
|
| T-13-16 | T | upload conflict handling | mitigate | Typed upload result bodies and provider-contract tests forbid silent overwrite behavior. |
|
||||||
|
| T-13-17 | I | route result typing | mitigate | Upload route tests verify no provider URLs, tokens, or raw payloads escape typed responses. |
|
||||||
|
| T-13-18 | T | provider boundary | mitigate | WebDAV SSRF and OneDrive refresh handoff remain covered by provider suites. |
|
||||||
|
</threat_model>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- Pass the bounded backend upload and provider-contract suites.
|
||||||
|
- Confirm all backend pytest invocations are direct containerized commands.
|
||||||
|
- Confirm upload mechanics stop at typed route and service results, leaving reconcile and audit follow-through to the next plan.
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- The backend can upload into the current cloud folder with typed conflict and retry semantics.
|
||||||
|
- Provider-specific differences remain behind the shared contract and service layer.
|
||||||
|
- The upload mechanics plan stays within 9 files and leaves reconcile plus audit follow-through for the next bounded plan.
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
Create `.planning/phases/13-virtual-local-cloud-operations/13-05-SUMMARY.md` when done
|
||||||
|
</output>
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
---
|
||||||
|
phase: "13"
|
||||||
|
plan: "06"
|
||||||
|
type: execute
|
||||||
|
wave: 4
|
||||||
|
depends_on:
|
||||||
|
- "13-05"
|
||||||
|
files_modified:
|
||||||
|
- backend/api/cloud/operations.py
|
||||||
|
- backend/services/cloud_operations.py
|
||||||
|
- backend/services/cloud_items.py
|
||||||
|
- backend/tests/test_cloud_mutations.py
|
||||||
|
- backend/tests/test_cloud_audit.py
|
||||||
|
autonomous: true
|
||||||
|
requirements:
|
||||||
|
- CLOUD-03
|
||||||
|
- CLOUD-09
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "Successful cloud upload does not return before authoritative metadata reconciliation completes."
|
||||||
|
- "Upload success writes metadata-only audit rows, while skipped, canceled, or failed queue decisions stay silent."
|
||||||
|
- "Upload freshness and navigation updates still route through `cloud_items.py` rather than a second reconcile path."
|
||||||
|
artifacts:
|
||||||
|
- path: "backend/services/cloud_operations.py"
|
||||||
|
provides: "Authoritative upload success handling that routes back through centralized reconciliation and audit helpers."
|
||||||
|
- path: "backend/services/cloud_items.py"
|
||||||
|
provides: "Stable folder freshness and item identity updates after upload success."
|
||||||
|
key_links:
|
||||||
|
- from: "authoritative upload success"
|
||||||
|
to: "cloud_items reconciliation"
|
||||||
|
via: "service-layer follow-through before response return"
|
||||||
|
pattern: "upload"
|
||||||
|
- from: "authoritative upload success"
|
||||||
|
to: "metadata-only audit rows"
|
||||||
|
via: "same-transaction audit helper call"
|
||||||
|
pattern: "cloud.item"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Finish the bounded upload follow-through slice of Phase 13: reconcile successful uploads immediately, refresh affected folder state truthfully, and emit metadata-only audit rows only on authoritative success.
|
||||||
|
|
||||||
|
Purpose: Complete `CLOUD-09` for uploads before the shared frontend queue is wired to the backend.
|
||||||
|
Output: Passing upload reconciliation and audit suites with no duplicate reconcile path.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@$HOME/.codex/gsd-core/workflows/execute-plan.md
|
||||||
|
@$HOME/.codex/gsd-core/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@AGENTS.md
|
||||||
|
@.planning/phases/13-virtual-local-cloud-operations/13-CONTEXT.md
|
||||||
|
@backend/api/cloud/operations.py
|
||||||
|
@backend/services/cloud_operations.py
|
||||||
|
@backend/services/cloud_items.py
|
||||||
|
@backend/services/audit.py
|
||||||
|
@backend/tests/test_cloud_mutations.py
|
||||||
|
@backend/tests/test_cloud_audit.py
|
||||||
|
</context>
|
||||||
|
|
||||||
|
## Artifacts this phase produces
|
||||||
|
|
||||||
|
- Upload success follow-through in `backend/services/cloud_operations.py`.
|
||||||
|
- Authoritative upload reconciliation and folder freshness updates through `backend/services/cloud_items.py`.
|
||||||
|
- Passing upload mutation and audit suites for `CLOUD-03` and `CLOUD-09`.
|
||||||
|
|
||||||
|
## Pattern analogs
|
||||||
|
|
||||||
|
- `backend/services/cloud_items.py` — single reconciliation and freshness authority.
|
||||||
|
- `backend/services/audit.py` — metadata-only audit helper used inside the caller transaction.
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto" tdd="true">
|
||||||
|
<name>Task 1: Route authoritative upload success through centralized reconciliation before returning</name>
|
||||||
|
<files>backend/api/cloud/operations.py, backend/services/cloud_operations.py, backend/services/cloud_items.py, backend/tests/test_cloud_mutations.py</files>
|
||||||
|
<read_first>
|
||||||
|
- backend/services/cloud_operations.py
|
||||||
|
- backend/services/cloud_items.py
|
||||||
|
- backend/tests/test_cloud_mutations.py
|
||||||
|
- .planning/phases/13-virtual-local-cloud-operations/13-CONTEXT.md
|
||||||
|
</read_first>
|
||||||
|
<behavior>
|
||||||
|
- Test 1: successful upload updates current-folder navigation through centralized reconciliation before success returns
|
||||||
|
- Test 2: upload freshness state stays truthful and does not bypass `cloud_items.py`
|
||||||
|
- Test 3: failed, skipped, and canceled queue decisions do not mutate listing freshness as success
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
Feed authoritative upload success back through `backend/services/cloud_items.py` immediately, update affected folder freshness truthfully, and keep stable row identity for the newly visible item before the route returns success. Do not bypass centralized reconciliation, do not mutate quota, and do not let skipped or canceled queue decisions look like successful overwrites.
|
||||||
|
</action>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- upload mutation suites pass with reconcile-before-return behavior
|
||||||
|
- navigation refresh is tied to authoritative success only
|
||||||
|
- upload follow-through still uses the single shared reconciliation path
|
||||||
|
</acceptance_criteria>
|
||||||
|
<verify>
|
||||||
|
<automated>docker compose run --rm backend pytest -v tests/test_cloud_mutations.py -k "upload" -x</automated>
|
||||||
|
</verify>
|
||||||
|
<done>Successful uploads now refresh authoritative metadata and freshness before the API reports success.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto" tdd="true">
|
||||||
|
<name>Task 2: Emit metadata-only audit rows only for authoritative upload success</name>
|
||||||
|
<files>backend/services/cloud_operations.py, backend/tests/test_cloud_mutations.py, backend/tests/test_cloud_audit.py</files>
|
||||||
|
<read_first>
|
||||||
|
- backend/services/cloud_operations.py
|
||||||
|
- backend/services/audit.py
|
||||||
|
- backend/tests/test_cloud_audit.py
|
||||||
|
- .planning/phases/13-virtual-local-cloud-operations/13-CONTEXT.md
|
||||||
|
</read_first>
|
||||||
|
<behavior>
|
||||||
|
- Test 1: successful upload writes metadata-only audit rows in the caller transaction
|
||||||
|
- Test 2: failed, skipped, canceled, and retry-needed queue outcomes do not produce false success audit events
|
||||||
|
- Test 3: audit rows never include provider URLs, tokens, bytes, or document text
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
Write metadata-only audit rows in the same request transaction only after authoritative upload success has reconciled metadata. Preserve the Phase 13 secrecy rules by keeping provider URLs, tokens, decrypted credentials, bytes, and document content out of the audit payload. Treat Skip, Cancel all, retryable error, and conflict-pause outcomes as non-success paths that must not emit a success audit event.
|
||||||
|
</action>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- upload audit suites pass
|
||||||
|
- audit behavior is tied to authoritative success only
|
||||||
|
- no upload audit row includes provider URLs, tokens, bytes, or document text
|
||||||
|
</acceptance_criteria>
|
||||||
|
<verify>
|
||||||
|
<automated>docker compose run --rm backend pytest -v tests/test_cloud_mutations.py tests/test_cloud_audit.py -k "upload or audit" -x</automated>
|
||||||
|
</verify>
|
||||||
|
<done>Upload success now satisfies the metadata refresh and metadata-only audit half of `CLOUD-09`.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<threat_model>
|
||||||
|
## Trust Boundaries
|
||||||
|
|
||||||
|
| Boundary | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| upload success → metadata state | Only authoritative success may update cloud metadata or folder freshness. |
|
||||||
|
| upload success → audit log | Only authoritative success may write an audit row, and it must stay metadata-only. |
|
||||||
|
|
||||||
|
## STRIDE Threat Register
|
||||||
|
|
||||||
|
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||||
|
|-----------|----------|-----------|-------------|-----------------|
|
||||||
|
| T-13-19 | T | upload reconciliation | mitigate | Mutation tests require reconcile-before-return and forbid success-state bypasses. |
|
||||||
|
| T-13-20 | I | upload audit payloads | mitigate | Audit suites verify metadata-only rows for successful uploads only. |
|
||||||
|
| T-13-21 | R | queue decision logging | mitigate | Tests require Skip, Cancel all, conflict-pause, and retry-needed outcomes to avoid false success events. |
|
||||||
|
</threat_model>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- Pass the upload mutation and audit suites.
|
||||||
|
- Confirm every backend pytest invocation is a direct `docker compose run --rm backend pytest ...` command.
|
||||||
|
- Confirm upload success routes through centralized reconciliation before audit and before response return.
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- Successful uploads refresh navigation and folder freshness through the shared reconciliation layer.
|
||||||
|
- Successful uploads produce metadata-only audit rows promptly enough to satisfy `CLOUD-09`.
|
||||||
|
- The upload follow-through plan stays bounded to 5 files and does not absorb the frontend queue work.
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
Create `.planning/phases/13-virtual-local-cloud-operations/13-06-SUMMARY.md` when done
|
||||||
|
</output>
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
---
|
||||||
|
phase: "13"
|
||||||
|
plan: "07"
|
||||||
|
type: execute
|
||||||
|
wave: 5
|
||||||
|
depends_on:
|
||||||
|
- "13-02"
|
||||||
|
- "13-04"
|
||||||
|
- "13-06"
|
||||||
|
files_modified:
|
||||||
|
- frontend/src/api/cloud.js
|
||||||
|
- frontend/src/views/CloudFolderView.vue
|
||||||
|
- frontend/src/components/storage/StorageBrowser.vue
|
||||||
|
- frontend/src/components/storage/__tests__/StorageBrowser.cloud-queue.test.js
|
||||||
|
- frontend/src/views/__tests__/CloudFolderOpenPreview.test.js
|
||||||
|
- frontend/src/views/__tests__/CloudFolderView.test.js
|
||||||
|
autonomous: true
|
||||||
|
requirements:
|
||||||
|
- CLOUD-02
|
||||||
|
- CLOUD-03
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "Cloud upload stays on the shared browser path and never forks a cloud-only queue UI."
|
||||||
|
- "The queue is sequential and pauses on typed conflict or error bodies until the user explicitly resumes or cancels all."
|
||||||
|
- "Binary-only preview and authorized download fallback are driven through centralized client helpers, not provider URLs."
|
||||||
|
artifacts:
|
||||||
|
- path: "frontend/src/views/CloudFolderView.vue"
|
||||||
|
provides: "Thin queue and preview orchestration over the shared browser."
|
||||||
|
- path: "frontend/src/components/storage/StorageBrowser.vue"
|
||||||
|
provides: "Shared queue dialogs and authorized content action surfaces."
|
||||||
|
key_links:
|
||||||
|
- from: "upload and content client helpers"
|
||||||
|
to: "shared browser events"
|
||||||
|
via: "CloudFolderView thin handlers"
|
||||||
|
pattern: "upload"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Wire the shared browser to the completed backend upload and content endpoints so cloud queue handling, binary-only preview, and authorized fallback download behave exactly like the new Phase 13 contracts require.
|
||||||
|
|
||||||
|
Purpose: Complete the frontend half of `CLOUD-02` and `CLOUD-03` without violating the shared-browser architecture.
|
||||||
|
Output: Passing queue, preview, and CloudFolderView frontend suites.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@$HOME/.codex/gsd-core/workflows/execute-plan.md
|
||||||
|
@$HOME/.codex/gsd-core/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@AGENTS.md
|
||||||
|
@.planning/phases/13-virtual-local-cloud-operations/13-CONTEXT.md
|
||||||
|
@.planning/phases/13-virtual-local-cloud-operations/13-PATTERNS.md
|
||||||
|
@frontend/src/api/cloud.js
|
||||||
|
@frontend/src/views/CloudFolderView.vue
|
||||||
|
@frontend/src/components/storage/StorageBrowser.vue
|
||||||
|
@frontend/src/components/storage/__tests__/StorageBrowser.cloud-queue.test.js
|
||||||
|
@frontend/src/views/__tests__/CloudFolderOpenPreview.test.js
|
||||||
|
@frontend/src/views/__tests__/CloudFolderView.test.js
|
||||||
|
</context>
|
||||||
|
|
||||||
|
## Artifacts this phase produces
|
||||||
|
|
||||||
|
- Shared upload queue and conflict-dialog behavior in `StorageBrowser.vue`.
|
||||||
|
- Thin queue and preview orchestration in `CloudFolderView.vue`.
|
||||||
|
- Passing queue, preview, and thin-view tests for `CLOUD-02` and `CLOUD-03`.
|
||||||
|
|
||||||
|
## Pattern analogs
|
||||||
|
|
||||||
|
- `frontend/src/views/FileManagerView.vue` — local thin-view orchestration style.
|
||||||
|
- `frontend/src/components/storage/StorageBrowser.vue` — existing shared action and dialog surface.
|
||||||
|
- `frontend/src/api/cloud.js` — centralized client boundary pattern.
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto" tdd="true">
|
||||||
|
<name>Task 1: Implement the sequential shared upload queue with typed pause and resume behavior</name>
|
||||||
|
<files>frontend/src/api/cloud.js, frontend/src/views/CloudFolderView.vue, frontend/src/components/storage/StorageBrowser.vue, frontend/src/components/storage/__tests__/StorageBrowser.cloud-queue.test.js, frontend/src/views/__tests__/CloudFolderView.test.js</files>
|
||||||
|
<read_first>
|
||||||
|
- frontend/src/views/FileManagerView.vue
|
||||||
|
- frontend/src/components/storage/StorageBrowser.vue
|
||||||
|
- frontend/src/views/CloudFolderView.vue
|
||||||
|
- frontend/src/components/storage/__tests__/StorageBrowser.cloud-queue.test.js
|
||||||
|
</read_first>
|
||||||
|
<behavior>
|
||||||
|
- Test 1: the queue runs one file at a time and pauses on typed conflict or error results
|
||||||
|
- Test 2: Keep both, Replace, Skip, Retry, and Cancel all resume or stop the queue exactly where required
|
||||||
|
- Test 3: CloudFolderView remains thin and delegates queue UI to the shared browser
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
Replace the placeholder cloud upload flow with sequential queue orchestration that calls the new upload helper in `frontend/src/api/cloud.js`, feeds typed conflict and error bodies into shared browser dialogs, and preserves remaining items while paused. Keep queue state in the thin-view plus shared-browser boundary only; do not create a cloud-only queue component and do not infer replace semantics client-side.
|
||||||
|
</action>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- queue and view tests pass using the shared browser rather than a parallel cloud UI
|
||||||
|
- typed backend conflict/error bodies drive pause and resume behavior directly
|
||||||
|
- Cancel all stops remaining uploads without recording a false overwrite success
|
||||||
|
</acceptance_criteria>
|
||||||
|
<verify>
|
||||||
|
<automated>cd frontend && npm run test -- --run src/components/storage/__tests__/StorageBrowser.cloud-queue.test.js src/views/__tests__/CloudFolderView.test.js</automated>
|
||||||
|
</verify>
|
||||||
|
<done>The shared browser now owns the sequential cloud upload experience without architectural drift.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto" tdd="true">
|
||||||
|
<name>Task 2: Wire binary-only preview and authorized download fallback through shared actions</name>
|
||||||
|
<files>frontend/src/api/cloud.js, frontend/src/views/CloudFolderView.vue, frontend/src/components/storage/StorageBrowser.vue, frontend/src/views/__tests__/CloudFolderOpenPreview.test.js</files>
|
||||||
|
<read_first>
|
||||||
|
- frontend/src/api/cloud.js
|
||||||
|
- frontend/src/views/__tests__/CloudFolderOpenPreview.test.js
|
||||||
|
- .planning/phases/13-virtual-local-cloud-operations/13-CONTEXT.md
|
||||||
|
</read_first>
|
||||||
|
<behavior>
|
||||||
|
- Test 1: preview stays in-app for supported binary files
|
||||||
|
- Test 2: unsupported formats use authorized download fallback rather than Office-native or Google Workspace preview
|
||||||
|
- Test 3: no provider URL or credential is stored or opened directly in Vue
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
Implement the cloud open and preview handlers through centralized client helpers only. Keep the preview path limited to the backend-declared binary matrix and route unsupported formats to the authorized download fallback. Reuse the shared browser’s action surface for all user interactions so the cloud path feels local while still respecting the stricter backend content contract.
|
||||||
|
</action>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- preview suite passes with binary-only in-app preview and authorized fallback download
|
||||||
|
- CloudFolderView remains a thin data provider
|
||||||
|
- no frontend code opens raw provider URLs or stores provider credentials
|
||||||
|
</acceptance_criteria>
|
||||||
|
<verify>
|
||||||
|
<automated>cd frontend && npm run test -- --run src/views/__tests__/CloudFolderOpenPreview.test.js</automated>
|
||||||
|
</verify>
|
||||||
|
<done>The cloud browser now consumes the authorized preview and download contract through the same shared actions users already know.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<threat_model>
|
||||||
|
## Trust Boundaries
|
||||||
|
|
||||||
|
| Boundary | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| typed backend result → shared queue UI | The client must consume authoritative conflict/error typing instead of inventing semantics. |
|
||||||
|
| content helper → preview surface | Preview must stay behind DocuVault authorization. |
|
||||||
|
|
||||||
|
## STRIDE Threat Register
|
||||||
|
|
||||||
|
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||||
|
|-----------|----------|-----------|-------------|-----------------|
|
||||||
|
| T-13-22 | T | queue resume flow | mitigate | Shared-browser tests require explicit resume and cancel semantics for every pause reason. |
|
||||||
|
| T-13-23 | I | preview/download UI | mitigate | Preview tests forbid raw provider URLs and require binary-only in-app preview plus authorized fallback. |
|
||||||
|
</threat_model>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- Pass the new queue and preview frontend suites.
|
||||||
|
- Confirm the cloud path still uses `StorageBrowser.vue` as the single browser.
|
||||||
|
- Confirm preview remains binary-only and credential-safe.
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- Users can upload to cloud storage through the same shared browser interaction path as local files.
|
||||||
|
- Cloud preview and download fallback stay within the authorized backend contract.
|
||||||
|
- The frontend now satisfies `CLOUD-02` and `CLOUD-03` without architectural duplication.
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
Create `.planning/phases/13-virtual-local-cloud-operations/13-07-SUMMARY.md` when done
|
||||||
|
</output>
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
---
|
||||||
|
phase: "13"
|
||||||
|
plan: "08"
|
||||||
|
type: execute
|
||||||
|
wave: 5
|
||||||
|
depends_on:
|
||||||
|
- "13-03"
|
||||||
|
- "13-04"
|
||||||
|
- "13-06"
|
||||||
|
files_modified:
|
||||||
|
- backend/api/cloud/operations.py
|
||||||
|
- backend/api/cloud/schemas.py
|
||||||
|
- backend/services/cloud_operations.py
|
||||||
|
- backend/services/cloud_items.py
|
||||||
|
- backend/storage/google_drive_backend.py
|
||||||
|
- backend/storage/onedrive_backend.py
|
||||||
|
- backend/storage/webdav_backend.py
|
||||||
|
- backend/tests/test_cloud_mutations.py
|
||||||
|
- backend/tests/test_cloud_backends.py
|
||||||
|
- backend/tests/test_cloud_provider_contract.py
|
||||||
|
autonomous: true
|
||||||
|
requirements:
|
||||||
|
- CLOUD-04
|
||||||
|
- CLOUD-05
|
||||||
|
- CLOUD-09
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "Create-folder and rename collisions auto-suffix human-readable counters with bounded retry."
|
||||||
|
- "Stale create or rename targets stop, refresh the affected folder, and return typed retry guidance instead of forcing the mutation."
|
||||||
|
- "Successful create and rename operations preserve stable cloud item identity through centralized reconciliation."
|
||||||
|
artifacts:
|
||||||
|
- path: "backend/api/cloud/operations.py"
|
||||||
|
provides: "Create-folder and rename endpoints with typed collision and stale-result bodies."
|
||||||
|
- path: "backend/services/cloud_operations.py"
|
||||||
|
provides: "Create-folder and rename orchestration with bounded retry and reconcile-on-success behavior."
|
||||||
|
key_links:
|
||||||
|
- from: "create-folder and rename provider result"
|
||||||
|
to: "cloud_items stable row identity"
|
||||||
|
via: "centralized post-mutation reconciliation"
|
||||||
|
pattern: "parent_ref"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Implement the bounded backend create-folder and rename slice of Phase 13: collision-safe naming, stale guards, bounded retry, and stable-identity reconciliation for successful results.
|
||||||
|
|
||||||
|
Purpose: Deliver `CLOUD-04` and `CLOUD-05` without mixing move or delete semantics into the same plan.
|
||||||
|
Output: Passing backend create-folder and rename mutation suites with centralized reconciliation intact.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@$HOME/.codex/gsd-core/workflows/execute-plan.md
|
||||||
|
@$HOME/.codex/gsd-core/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@AGENTS.md
|
||||||
|
@.planning/phases/13-virtual-local-cloud-operations/13-CONTEXT.md
|
||||||
|
@.planning/phases/13-virtual-local-cloud-operations/13-RESEARCH.md
|
||||||
|
@backend/api/cloud/operations.py
|
||||||
|
@backend/services/cloud_operations.py
|
||||||
|
@backend/services/cloud_items.py
|
||||||
|
@backend/storage/google_drive_backend.py
|
||||||
|
@backend/storage/onedrive_backend.py
|
||||||
|
@backend/storage/webdav_backend.py
|
||||||
|
</context>
|
||||||
|
|
||||||
|
## Artifacts this phase produces
|
||||||
|
|
||||||
|
- Create-folder and rename support in the operations router and cloud operations service.
|
||||||
|
- Four-provider collision, bounded-retry, and stale-result normalization through Google Drive, OneDrive, Nextcloud-via-WebDAV, and generic WebDAV.
|
||||||
|
- Passing create-folder and rename backend, provider-contract, and mutation suites.
|
||||||
|
|
||||||
|
## Pattern analogs
|
||||||
|
|
||||||
|
- `backend/api/folders.py` — local create and rename handler structure.
|
||||||
|
- `backend/services/cloud_items.py` — stable identity and freshness reconciliation behavior.
|
||||||
|
- `backend/tests/test_cloud_backends.py` and `backend/tests/test_cloud_provider_contract.py` — provider normalization assertions.
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto" tdd="true">
|
||||||
|
<name>Task 1: Implement create-folder and rename semantics with bounded collision retries and stale guards</name>
|
||||||
|
<files>backend/api/cloud/operations.py, backend/api/cloud/schemas.py, backend/services/cloud_operations.py, backend/storage/google_drive_backend.py, backend/storage/onedrive_backend.py, backend/storage/webdav_backend.py, backend/tests/test_cloud_mutations.py, backend/tests/test_cloud_backends.py, backend/tests/test_cloud_provider_contract.py</files>
|
||||||
|
<read_first>
|
||||||
|
- backend/api/folders.py
|
||||||
|
- backend/storage/google_drive_backend.py
|
||||||
|
- backend/storage/onedrive_backend.py
|
||||||
|
- backend/storage/webdav_backend.py
|
||||||
|
- backend/tests/test_cloud_mutations.py
|
||||||
|
- .planning/phases/13-virtual-local-cloud-operations/13-CONTEXT.md
|
||||||
|
</read_first>
|
||||||
|
<behavior>
|
||||||
|
- Test 1: create-folder and rename choose `Name (n)` counters and retry within a bounded window after concurrent collisions
|
||||||
|
- Test 2: stale version or etag mismatches stop the mutation, refresh the folder, and return a typed retry-needed result
|
||||||
|
- Test 3: provider differences normalize behind one shared contract
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
Implement D-05 through D-07 for create-folder and rename. Use the shared mutable-operation contract to normalize collision detection, bounded retry, and stale precondition failure behavior across Google Drive, OneDrive, and the shared WebDAV path. Keep counter insertion human-readable and before the file extension for files, and keep Nextcloud on the shared WebDAV mutation path unless a narrow override is unavoidable.
|
||||||
|
</action>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- create-folder and rename suites pass across provider-contract and backend mutation coverage
|
||||||
|
- stale mismatches return typed retry-needed results after refresh rather than forcing the mutation
|
||||||
|
- no provider writes `cloud_items` directly or bypasses the shared contract
|
||||||
|
</acceptance_criteria>
|
||||||
|
<verify>
|
||||||
|
<automated>docker compose run --rm backend pytest -v tests/test_cloud_mutations.py tests/test_cloud_backends.py tests/test_cloud_provider_contract.py -k "create or rename or stale" -x</automated>
|
||||||
|
</verify>
|
||||||
|
<done>Create-folder and rename now satisfy the bounded collision and stale-safety rules.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto" tdd="true">
|
||||||
|
<name>Task 2: Reconcile successful create-folder and rename results through stable item identity</name>
|
||||||
|
<files>backend/services/cloud_operations.py, backend/services/cloud_items.py, backend/tests/test_cloud_mutations.py</files>
|
||||||
|
<read_first>
|
||||||
|
- backend/services/cloud_operations.py
|
||||||
|
- backend/services/cloud_items.py
|
||||||
|
- backend/tests/test_cloud_mutations.py
|
||||||
|
- .planning/phases/13-virtual-local-cloud-operations/13-CONTEXT.md
|
||||||
|
</read_first>
|
||||||
|
<behavior>
|
||||||
|
- Test 1: successful create-folder and rename update navigation through centralized reconciliation before success returns
|
||||||
|
- Test 2: stable cloud item identity is preserved instead of creating duplicate rows
|
||||||
|
- Test 3: refresh guidance remains typed and tied to the service layer rather than provider-specific router branches
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
Route successful create-folder and rename results back through `backend/services/cloud_items.py` so stable row identity, parent relationships, and folder freshness stay authoritative before the API returns success. Keep the route layer thin, keep typed stale-retry guidance service-owned, and do not introduce a second reconciliation path for create or rename.
|
||||||
|
</action>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- create-folder and rename mutation suites pass with reconcile-before-return behavior
|
||||||
|
- stable row identity is preserved across successful create-folder and rename operations
|
||||||
|
- centralized reconciliation remains the only metadata write path
|
||||||
|
</acceptance_criteria>
|
||||||
|
<verify>
|
||||||
|
<automated>docker compose run --rm backend pytest -v tests/test_cloud_mutations.py -k "create or rename" -x</automated>
|
||||||
|
</verify>
|
||||||
|
<done>Create-folder and rename success now satisfy the stable-identity half of `CLOUD-09`.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<threat_model>
|
||||||
|
## Trust Boundaries
|
||||||
|
|
||||||
|
| Boundary | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| proposed new name → provider mutation | User-supplied names must be collision-safe, stale-safe, and provider-neutral before mutation. |
|
||||||
|
| mutation success → metadata state | Only authoritative success may update cloud metadata and folder freshness. |
|
||||||
|
|
||||||
|
## STRIDE Threat Register
|
||||||
|
|
||||||
|
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||||
|
|-----------|----------|-----------|-------------|-----------------|
|
||||||
|
| T-13-24 | T | collision handling | mitigate | Mutation and provider-contract suites require bounded retry and forbid silent overwrite behavior. |
|
||||||
|
| T-13-25 | T | stale mutation safety | mitigate | Stale guards refresh and require retry instead of forcing create-folder or rename. |
|
||||||
|
| T-13-26 | T | identity reconciliation | mitigate | Successful create-folder and rename must flow back through `cloud_items.py` to preserve stable row identity. |
|
||||||
|
</threat_model>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- Pass the create-folder and rename backend and provider-contract suites.
|
||||||
|
- Confirm every backend pytest invocation is a direct `docker compose run --rm backend pytest ...` command.
|
||||||
|
- Confirm create-folder and rename remain separated from move and delete work in both files touched and task scope.
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- The backend fully supports `CLOUD-04` and `CLOUD-05` with bounded collision and stale-safety semantics.
|
||||||
|
- Successful create-folder and rename results refresh navigation through centralized reconciliation.
|
||||||
|
- This bounded plan stays at 10 files and does not absorb move or delete behavior.
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
Create `.planning/phases/13-virtual-local-cloud-operations/13-08-SUMMARY.md` when done
|
||||||
|
</output>
|
||||||
@@ -172,3 +172,12 @@ No new security surfaces introduced. T-13-24, T-13-25, T-13-26 are mitigated:
|
|||||||
- FOUND commit: `9ad9946` (Task 1 RED)
|
- FOUND commit: `9ad9946` (Task 1 RED)
|
||||||
- FOUND commit: `aaa63c1` (Task 1/2 GREEN)
|
- FOUND commit: `aaa63c1` (Task 1/2 GREEN)
|
||||||
- Full suite: 755 passed, 17 skipped, 4 deselected, 12 xfailed
|
- Full suite: 755 passed, 17 skipped, 4 deselected, 12 xfailed
|
||||||
|
|
||||||
|
## Self-Check: PASSED (verified)
|
||||||
|
|
||||||
|
- FOUND: `backend/api/cloud/operations.py`
|
||||||
|
- FOUND: `backend/tests/test_cloud_mutations.py`
|
||||||
|
- FOUND: `.planning/phases/13-virtual-local-cloud-operations/13-08-SUMMARY.md`
|
||||||
|
- FOUND commit: `9ad9946` (RED tests)
|
||||||
|
- FOUND commit: `aaa63c1` (GREEN implementation)
|
||||||
|
- FOUND commit: `3e9355f` (docs/state)
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
---
|
||||||
|
phase: "13"
|
||||||
|
plan: "09"
|
||||||
|
type: execute
|
||||||
|
wave: 6
|
||||||
|
depends_on:
|
||||||
|
- "13-08"
|
||||||
|
files_modified:
|
||||||
|
- backend/api/cloud/operations.py
|
||||||
|
- backend/api/cloud/schemas.py
|
||||||
|
- backend/services/cloud_operations.py
|
||||||
|
- backend/services/cloud_items.py
|
||||||
|
- backend/storage/google_drive_backend.py
|
||||||
|
- backend/storage/onedrive_backend.py
|
||||||
|
- backend/storage/webdav_backend.py
|
||||||
|
- backend/tests/test_cloud_mutations.py
|
||||||
|
- backend/tests/test_cloud_security.py
|
||||||
|
- backend/tests/test_cloud_audit.py
|
||||||
|
autonomous: true
|
||||||
|
requirements:
|
||||||
|
- CLOUD-06
|
||||||
|
- CLOUD-07
|
||||||
|
- CLOUD-09
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "Move is restricted to one connection and rejects self or descendant destinations before and after provider submission."
|
||||||
|
- "Delete returns typed trash-versus-permanent disclosure and stronger folder warnings without leaking provider internals."
|
||||||
|
- "Successful move and delete operations reconcile metadata and emit metadata-only audit rows before success returns."
|
||||||
|
artifacts:
|
||||||
|
- path: "backend/api/cloud/operations.py"
|
||||||
|
provides: "Move and delete endpoints with destination, stale, and delete-disclosure safeguards."
|
||||||
|
- path: "backend/services/cloud_operations.py"
|
||||||
|
provides: "Move and delete orchestration tied to centralized reconciliation and metadata-only auditing."
|
||||||
|
key_links:
|
||||||
|
- from: "move and delete provider result"
|
||||||
|
to: "cloud_items stable row identity"
|
||||||
|
via: "centralized post-mutation reconciliation"
|
||||||
|
pattern: "parent_ref"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Implement the bounded backend move and delete slice of Phase 13: same-connection validation, stale guards, delete disclosure, metadata reconciliation, and metadata-only audit behavior.
|
||||||
|
|
||||||
|
Purpose: Deliver `CLOUD-06`, `CLOUD-07`, and the remaining mutation half of `CLOUD-09` without dragging create-folder or rename work back into scope.
|
||||||
|
Output: Passing backend move, delete, security, and audit suites.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@$HOME/.codex/gsd-core/workflows/execute-plan.md
|
||||||
|
@$HOME/.codex/gsd-core/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@AGENTS.md
|
||||||
|
@.planning/phases/13-virtual-local-cloud-operations/13-CONTEXT.md
|
||||||
|
@backend/api/cloud/operations.py
|
||||||
|
@backend/services/cloud_operations.py
|
||||||
|
@backend/services/cloud_items.py
|
||||||
|
@backend/storage/google_drive_backend.py
|
||||||
|
@backend/storage/onedrive_backend.py
|
||||||
|
@backend/storage/webdav_backend.py
|
||||||
|
@backend/tests/test_cloud_security.py
|
||||||
|
@backend/tests/test_cloud_audit.py
|
||||||
|
</context>
|
||||||
|
|
||||||
|
## Artifacts this phase produces
|
||||||
|
|
||||||
|
- Move and delete support in the operations router and cloud operations service.
|
||||||
|
- Four-provider destination, stale, trash-versus-permanent delete, and audit-safe success behavior.
|
||||||
|
- Passing move, delete, security, and audit suites.
|
||||||
|
|
||||||
|
## Pattern analogs
|
||||||
|
|
||||||
|
- `backend/api/folders.py` — local move and delete handler structure.
|
||||||
|
- `backend/services/cloud_items.py` — stable identity and freshness reconciliation behavior.
|
||||||
|
- `backend/tests/test_cloud_security.py` — wrong-owner and cross-connection negative tests.
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto" tdd="true">
|
||||||
|
<name>Task 1: Implement move with same-connection validation, stale guards, and centralized reconciliation</name>
|
||||||
|
<files>backend/api/cloud/operations.py, backend/api/cloud/schemas.py, backend/services/cloud_operations.py, backend/services/cloud_items.py, backend/storage/google_drive_backend.py, backend/storage/onedrive_backend.py, backend/storage/webdav_backend.py, backend/tests/test_cloud_mutations.py, backend/tests/test_cloud_security.py</files>
|
||||||
|
<read_first>
|
||||||
|
- backend/api/folders.py
|
||||||
|
- backend/services/cloud_items.py
|
||||||
|
- backend/tests/test_cloud_security.py
|
||||||
|
- backend/tests/test_cloud_mutations.py
|
||||||
|
- .planning/phases/13-virtual-local-cloud-operations/13-CONTEXT.md
|
||||||
|
</read_first>
|
||||||
|
<behavior>
|
||||||
|
- Test 1: move rejects cross-connection, self, and descendant destinations before and after provider submission
|
||||||
|
- Test 2: stale version or etag mismatches stop the move, refresh the folder, and return typed retry-needed guidance
|
||||||
|
- Test 3: successful move reconciles metadata before success returns
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
Implement D-07 through D-09 for move. Enforce same-connection-only movement, disable or reject self and descendant destinations, normalize stale precondition failures, and keep provider differences behind the shared contract for Google Drive, OneDrive, and the shared WebDAV path. Route successful move results back through `backend/services/cloud_items.py` so identity, parent linkage, and freshness remain centralized.
|
||||||
|
</action>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- move and security suites pass
|
||||||
|
- invalid destinations are rejected consistently in service and provider paths
|
||||||
|
- successful move uses centralized reconciliation before success returns
|
||||||
|
</acceptance_criteria>
|
||||||
|
<verify>
|
||||||
|
<automated>docker compose run --rm backend pytest -v tests/test_cloud_mutations.py tests/test_cloud_security.py -k "move or destination" -x</automated>
|
||||||
|
</verify>
|
||||||
|
<done>Move now satisfies the single-connection, descendant-safety, and stale-refresh rules.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto" tdd="true">
|
||||||
|
<name>Task 2: Implement delete disclosure and metadata-only audit-safe success paths</name>
|
||||||
|
<files>backend/api/cloud/operations.py, backend/api/cloud/schemas.py, backend/services/cloud_operations.py, backend/storage/google_drive_backend.py, backend/storage/onedrive_backend.py, backend/storage/webdav_backend.py, backend/tests/test_cloud_mutations.py, backend/tests/test_cloud_security.py, backend/tests/test_cloud_audit.py</files>
|
||||||
|
<read_first>
|
||||||
|
- backend/services/cloud_operations.py
|
||||||
|
- backend/tests/test_cloud_security.py
|
||||||
|
- backend/tests/test_cloud_audit.py
|
||||||
|
- .planning/phases/13-virtual-local-cloud-operations/13-CONTEXT.md
|
||||||
|
</read_first>
|
||||||
|
<behavior>
|
||||||
|
- Test 1: delete returns typed trash-versus-permanent disclosure for files and folders
|
||||||
|
- Test 2: folder delete messaging is stronger than file delete messaging
|
||||||
|
- Test 3: successful delete emits metadata-only audit rows before success returns
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
Implement D-10 and D-11 for delete. Normalize provider results so the frontend can distinguish trash versus permanent delete without leaking provider internals, keep folder delete semantics explicit, and write metadata-only audit rows only after authoritative success. Preserve all owner-scope, secret-secrecy, and no-byte guarantees from the Wave 0 security suites.
|
||||||
|
</action>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- delete, security, and audit suites pass
|
||||||
|
- delete success discloses trash versus permanent behavior without provider leakage
|
||||||
|
- no audit row includes provider URLs, tokens, bytes, or document text
|
||||||
|
</acceptance_criteria>
|
||||||
|
<verify>
|
||||||
|
<automated>docker compose run --rm backend pytest -v tests/test_cloud_mutations.py tests/test_cloud_security.py tests/test_cloud_audit.py -k "delete or audit" -x</automated>
|
||||||
|
</verify>
|
||||||
|
<done>Delete now satisfies the disclosure, security, and metadata-only audit rules.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<threat_model>
|
||||||
|
## Trust Boundaries
|
||||||
|
|
||||||
|
| Boundary | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| user-selected destination → provider mutation | Invalid folder destinations must be rejected before and after provider submission. |
|
||||||
|
| delete success → audit trail | Only authoritative success may write metadata-only delete events. |
|
||||||
|
|
||||||
|
## STRIDE Threat Register
|
||||||
|
|
||||||
|
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||||
|
|-----------|----------|-----------|-------------|-----------------|
|
||||||
|
| T-13-27 | T | move destinations | mitigate | Mutation and security suites enforce same-connection, self, and descendant rejection. |
|
||||||
|
| T-13-28 | T | stale move safety | mitigate | Stale guards refresh and require retry instead of forcing move. |
|
||||||
|
| T-13-29 | I | delete disclosure and audit | mitigate | Typed delete results and audit tests keep provider behavior transparent and metadata-only. |
|
||||||
|
</threat_model>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- Pass the move, delete, security, and audit suites.
|
||||||
|
- Confirm every backend pytest invocation is a direct `docker compose run --rm backend pytest ...` command.
|
||||||
|
- Confirm move and delete remain separated from create-folder and rename work in both files touched and task scope.
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- The backend fully supports `CLOUD-06` and `CLOUD-07` plus the remaining move/delete portion of `CLOUD-09`.
|
||||||
|
- Stale safety, destination validation, and delete disclosure are explicit and tested across all supported provider classes.
|
||||||
|
- This bounded plan stays at 10 files and does not absorb create-folder or rename behavior.
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
Create `.planning/phases/13-virtual-local-cloud-operations/13-09-SUMMARY.md` when done
|
||||||
|
</output>
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
---
|
||||||
|
phase: "13"
|
||||||
|
plan: "10"
|
||||||
|
type: execute
|
||||||
|
wave: 7
|
||||||
|
depends_on:
|
||||||
|
- "13-02"
|
||||||
|
- "13-04"
|
||||||
|
- "13-07"
|
||||||
|
- "13-09"
|
||||||
|
files_modified:
|
||||||
|
- frontend/src/stores/cloudConnections.js
|
||||||
|
- frontend/src/views/CloudFolderView.vue
|
||||||
|
- frontend/src/components/storage/StorageBrowser.vue
|
||||||
|
- frontend/src/components/settings/SettingsCloudTab.vue
|
||||||
|
- frontend/src/components/cloud/CloudCredentialModal.vue
|
||||||
|
- frontend/src/components/settings/__tests__/SettingsCloudTab.health.test.js
|
||||||
|
- frontend/src/views/__tests__/CloudFolderRenderedFlow.test.js
|
||||||
|
- frontend/src/stores/__tests__/cloudConnections.test.js
|
||||||
|
- frontend/src/views/__tests__/CloudFolderView.test.js
|
||||||
|
autonomous: true
|
||||||
|
requirements:
|
||||||
|
- CONN-01
|
||||||
|
- CONN-02
|
||||||
|
- CONN-03
|
||||||
|
- CLOUD-04
|
||||||
|
- CLOUD-05
|
||||||
|
- CLOUD-06
|
||||||
|
- CLOUD-07
|
||||||
|
- CLOUD-09
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "Connection health is visible near the browser and in Settings from one shared store mapping."
|
||||||
|
- "Post-connect, post-reconnect, and credential-failure flows re-evaluate health automatically, while ordinary navigation never probes health."
|
||||||
|
- "The shared browser surfaces cloud create, rename, move, and delete results without introducing a second browser or folder picker."
|
||||||
|
artifacts:
|
||||||
|
- path: "frontend/src/stores/cloudConnections.js"
|
||||||
|
provides: "Server health mapping, reconnect refresh coordination, and no-probe-on-navigation behavior."
|
||||||
|
- path: "frontend/src/components/settings/SettingsCloudTab.vue"
|
||||||
|
provides: "Test/Reconnect/Disconnect controls and broader Google Drive consent copy."
|
||||||
|
key_links:
|
||||||
|
- from: "reconnect and credential-failure responses"
|
||||||
|
to: "browser-adjacent and Settings health state"
|
||||||
|
via: "cloudConnections store"
|
||||||
|
pattern: "health"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Finish the frontend side of Phase 13 by wiring shared-browser folder mutations, actionable health UX, broader Drive consent copy, automatic post-failure health rechecks, and the explicit no-probe-on-navigation rule.
|
||||||
|
|
||||||
|
Purpose: Close the loop between the completed backend contracts and the two frontend surfaces users rely on.
|
||||||
|
Output: Passing health, rendered-flow, store, and mutation UX suites.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@$HOME/.codex/gsd-core/workflows/execute-plan.md
|
||||||
|
@$HOME/.codex/gsd-core/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@AGENTS.md
|
||||||
|
@.planning/phases/13-virtual-local-cloud-operations/13-CONTEXT.md
|
||||||
|
@.planning/phases/13-virtual-local-cloud-operations/13-PATTERNS.md
|
||||||
|
@frontend/src/stores/cloudConnections.js
|
||||||
|
@frontend/src/views/CloudFolderView.vue
|
||||||
|
@frontend/src/components/storage/StorageBrowser.vue
|
||||||
|
@frontend/src/components/settings/SettingsCloudTab.vue
|
||||||
|
@frontend/src/components/cloud/CloudCredentialModal.vue
|
||||||
|
</context>
|
||||||
|
|
||||||
|
## Artifacts this phase produces
|
||||||
|
|
||||||
|
- Shared browser support for cloud create, rename, move, and delete UX.
|
||||||
|
- Store-backed browser and Settings health presentation with auto-test and no-probe behavior.
|
||||||
|
- Explicit broader Google Drive consent or reconnect copy in the cloud credential and settings surfaces.
|
||||||
|
|
||||||
|
## Pattern analogs
|
||||||
|
|
||||||
|
- `frontend/src/stores/__tests__/cloudConnections.test.js` — centralized state mapping pattern.
|
||||||
|
- `frontend/src/views/__tests__/CloudFolderRenderedFlow.test.js` — rendered browser-health flow pattern.
|
||||||
|
- `frontend/src/components/settings/__tests__/SettingsCloudTab.health.test.js` — Settings control and copy assertions.
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto" tdd="true">
|
||||||
|
<name>Task 1: Implement store-backed health and reconnect UX with explicit no-probe-on-navigation behavior</name>
|
||||||
|
<files>frontend/src/stores/cloudConnections.js, frontend/src/views/CloudFolderView.vue, frontend/src/components/settings/SettingsCloudTab.vue, frontend/src/components/cloud/CloudCredentialModal.vue, frontend/src/components/settings/__tests__/SettingsCloudTab.health.test.js, frontend/src/stores/__tests__/cloudConnections.test.js, frontend/src/views/__tests__/CloudFolderView.test.js</files>
|
||||||
|
<read_first>
|
||||||
|
- frontend/src/stores/cloudConnections.js
|
||||||
|
- frontend/src/components/settings/SettingsCloudTab.vue
|
||||||
|
- frontend/src/components/cloud/CloudCredentialModal.vue
|
||||||
|
- frontend/src/stores/__tests__/cloudConnections.test.js
|
||||||
|
- .planning/phases/13-virtual-local-cloud-operations/13-CONTEXT.md
|
||||||
|
</read_first>
|
||||||
|
<behavior>
|
||||||
|
- Test 1: browser-adjacent and Settings health views derive from one store mapping
|
||||||
|
- Test 2: connect, reconnect, and credential-failure flows auto-test health, but ordinary folder navigation never triggers a probe
|
||||||
|
- Test 3: broader Google Drive access is explicit in consent or reconnect copy
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
Extend the cloud connection store and thin view handlers so server health states are mapped once and rendered in both the cloud browser context and Settings. Trigger automatic health re-evaluation after connect, reconnect, and credential-related failures, keep cached metadata visible while warning or reauth states are shown, and explicitly prevent ordinary folder navigation from performing a provider health probe. Update the credential and Settings surfaces so Google Drive’s broader Phase 13 access request is visible in user-facing copy rather than hidden in backend-only behavior.
|
||||||
|
</action>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- health and store suites pass with explicit no-probe-on-navigation coverage
|
||||||
|
- browser and Settings show actionable, consistent health state from the same store mapping
|
||||||
|
- broader Drive access copy is visible wherever users connect or reconnect that provider
|
||||||
|
</acceptance_criteria>
|
||||||
|
<verify>
|
||||||
|
<automated>cd frontend && npm run test -- --run src/components/settings/__tests__/SettingsCloudTab.health.test.js src/stores/__tests__/cloudConnections.test.js src/views/__tests__/CloudFolderView.test.js</automated>
|
||||||
|
</verify>
|
||||||
|
<done>Connection health, reconnect, and consent UX now follow one store-backed truth without background probe drift.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto" tdd="true">
|
||||||
|
<name>Task 2: Surface folder mutation results through the shared browser without forking layout</name>
|
||||||
|
<files>frontend/src/views/CloudFolderView.vue, frontend/src/components/storage/StorageBrowser.vue, frontend/src/views/__tests__/CloudFolderRenderedFlow.test.js</files>
|
||||||
|
<read_first>
|
||||||
|
- frontend/src/components/storage/StorageBrowser.vue
|
||||||
|
- frontend/src/views/CloudFolderRenderedFlow.test.js
|
||||||
|
- .planning/phases/13-virtual-local-cloud-operations/13-CONTEXT.md
|
||||||
|
</read_first>
|
||||||
|
<behavior>
|
||||||
|
- Test 1: cloud create, rename, move, and delete reuse the shared browser surface and picker behavior
|
||||||
|
- Test 2: invalid destinations are disabled before submission and backend rejections are rendered clearly
|
||||||
|
- Test 3: trash-versus-permanent delete messaging and stale-refresh retry guidance are shown without duplicating layout
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
Wire the shared browser to consume the folder-mutation contract from the backend. Reuse the existing create, rename, drag-move, picker-move, and delete surfaces, but feed them cloud-specific capability, invalid-destination, stale-refresh, and trash-versus-permanent delete messages through props and emitted events only. Keep `CloudFolderView.vue` thin and avoid introducing a second folder picker, second drag state, or parallel mutation layout.
|
||||||
|
</action>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- rendered-flow suite passes with the real shared browser
|
||||||
|
- invalid destinations are prevented in the UI before submission and still handled cleanly after backend rejection
|
||||||
|
- cloud create/rename/move/delete behavior stays on the shared browser surface
|
||||||
|
</acceptance_criteria>
|
||||||
|
<verify>
|
||||||
|
<automated>cd frontend && npm run test -- --run src/views/__tests__/CloudFolderRenderedFlow.test.js</automated>
|
||||||
|
</verify>
|
||||||
|
<done>The frontend now presents full cloud mutation and health behavior through the shared browser and Settings surfaces only.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<threat_model>
|
||||||
|
## Trust Boundaries
|
||||||
|
|
||||||
|
| Boundary | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| server health/mutation results → store/UI | The UI must present backend truth without inventing probe or mutation semantics. |
|
||||||
|
| shared browser interactions → destructive actions | Delete and move UX must stay explicit and capability-aware. |
|
||||||
|
|
||||||
|
## STRIDE Threat Register
|
||||||
|
|
||||||
|
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||||
|
|-----------|----------|-----------|-------------|-----------------|
|
||||||
|
| T-13-30 | T | health UX | mitigate | Store and Settings tests require auto-test on connect/reconnect/failure and forbid navigation-triggered probes. |
|
||||||
|
| T-13-31 | R | destructive cloud actions | mitigate | Rendered-flow tests require explicit delete disclosure, stale-retry messaging, and invalid-destination prevention. |
|
||||||
|
| T-13-32 | I | broader-scope consent | mitigate | Settings and credential-modal tests require explicit broader Google Drive access copy. |
|
||||||
|
</threat_model>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- Pass the health, store, view, and rendered-flow frontend suites.
|
||||||
|
- Confirm the shared browser remains the only browser surface.
|
||||||
|
- Confirm no-probe-on-navigation is explicitly asserted and implemented.
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- Users can see, test, reconnect, and recover cloud connections through one consistent frontend state model.
|
||||||
|
- Shared-browser folder mutations now satisfy the backend contracts for `CLOUD-04` through `CLOUD-07`.
|
||||||
|
- Phase 13 now concretely encodes broader Drive access and the D-13 health-testing invariant on the frontend.
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
Create `.planning/phases/13-virtual-local-cloud-operations/13-10-SUMMARY.md` when done
|
||||||
|
</output>
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
---
|
||||||
|
phase: "13"
|
||||||
|
plan: "11"
|
||||||
|
type: execute
|
||||||
|
wave: 8
|
||||||
|
depends_on:
|
||||||
|
- "13-03"
|
||||||
|
- "13-04"
|
||||||
|
- "13-05"
|
||||||
|
- "13-06"
|
||||||
|
- "13-07"
|
||||||
|
- "13-08"
|
||||||
|
- "13-09"
|
||||||
|
- "13-10"
|
||||||
|
files_modified:
|
||||||
|
- .planning/ROADMAP.md
|
||||||
|
- AGENTS.md
|
||||||
|
- README.md
|
||||||
|
- RUNBOOK.md
|
||||||
|
- SECURITY.md
|
||||||
|
- backend/main.py
|
||||||
|
- frontend/package.json
|
||||||
|
autonomous: true
|
||||||
|
requirements:
|
||||||
|
- CONN-01
|
||||||
|
- CONN-02
|
||||||
|
- CONN-03
|
||||||
|
- CLOUD-02
|
||||||
|
- CLOUD-03
|
||||||
|
- CLOUD-04
|
||||||
|
- CLOUD-05
|
||||||
|
- CLOUD-06
|
||||||
|
- CLOUD-07
|
||||||
|
- CLOUD-09
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "Phase 13 closeout is isolated from implementation work."
|
||||||
|
- "Documentation, security evidence, version bumps, and ship gates happen only after all scoped behavior exists."
|
||||||
|
- "Full backend verification uses direct containerized pytest commands only, and the hardcoded-secret scan is non-skippable."
|
||||||
|
artifacts:
|
||||||
|
- path: "AGENTS.md"
|
||||||
|
provides: "Updated current-state line and shared-module guidance for shipped Phase 13 behavior."
|
||||||
|
- path: "SECURITY.md"
|
||||||
|
provides: "Phase 13 gate evidence for IDOR, CSRF, SSRF, typed conflict handling, audit secrecy, no-probe behavior, and the passing secret scan."
|
||||||
|
- path: ".planning/ROADMAP.md"
|
||||||
|
provides: "Phase 13 plan list and closeout status updates."
|
||||||
|
key_links:
|
||||||
|
- from: "completed Phase 13 behavior"
|
||||||
|
to: "docs, versions, and security evidence"
|
||||||
|
via: "final closeout-only plan"
|
||||||
|
pattern: "Phase 13"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Run the Phase 13 closeout only after all implementation plans are complete: update roadmap and docs, bump versions, run the full suites and security gates, execute a non-skippable hardcoded-secret scan, and prepare the phase for atomic commit and push.
|
||||||
|
|
||||||
|
Purpose: Keep documentation, versioning, security review, and ship gates isolated in one bounded final plan.
|
||||||
|
Output: Ready-to-ship Phase 13 documentation and validation state.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@$HOME/.codex/gsd-core/workflows/execute-plan.md
|
||||||
|
@$HOME/.codex/gsd-core/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@AGENTS.md
|
||||||
|
@.planning/ROADMAP.md
|
||||||
|
@.planning/REQUIREMENTS.md
|
||||||
|
@README.md
|
||||||
|
@RUNBOOK.md
|
||||||
|
@SECURITY.md
|
||||||
|
@backend/main.py
|
||||||
|
@frontend/package.json
|
||||||
|
</context>
|
||||||
|
|
||||||
|
## Artifacts this phase produces
|
||||||
|
|
||||||
|
- Updated roadmap and phase-close documentation.
|
||||||
|
- Version bumps for the shipped Phase 13 user-facing behavior.
|
||||||
|
- Full Phase 13 validation, dependency-audit, and security-gate evidence collected in one place, including a passing hardcoded-secret scan.
|
||||||
|
|
||||||
|
## Pattern analogs
|
||||||
|
|
||||||
|
- `.planning/phases/12.1-fix-nextcloud-root-listing-and-sync-visibility/12.1-04-SUMMARY.md` — bounded closeout-only phase finish.
|
||||||
|
- `AGENTS.md` documentation protocol — required current-state, shared-module, and version-bump updates.
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Update roadmap, docs, versions, and closeout evidence to match the shipped Phase 13 behavior</name>
|
||||||
|
<files>.planning/ROADMAP.md, AGENTS.md, README.md, RUNBOOK.md, SECURITY.md, backend/main.py, frontend/package.json</files>
|
||||||
|
<read_first>
|
||||||
|
- AGENTS.md
|
||||||
|
- .planning/ROADMAP.md
|
||||||
|
- README.md
|
||||||
|
- RUNBOOK.md
|
||||||
|
- SECURITY.md
|
||||||
|
</read_first>
|
||||||
|
<action>
|
||||||
|
Update Phase 13 planning and closeout documentation only after Plans 03 through 10 are complete. Record the finished health, reconnect, content, upload, create-folder, rename, move, delete, broader Drive scope, binary-only preview, and no-probe-on-navigation behavior accurately. Update the AGENTS current-state line and shared module map if the cloud operations layer became a new canonical shared module. Bump backend and frontend patch versions together for the shipped user-visible behavior, and record the final gate evidence in `SECURITY.md`, including the required hardcoded-secret scan result.
|
||||||
|
</action>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- roadmap and docs describe the actual shipped Phase 13 scope without claiming Phase 14 cache lifecycle or deferred Collabora work
|
||||||
|
- AGENTS and README reflect the final user-visible behavior and version numbers accurately
|
||||||
|
- SECURITY captures the Phase 13 threat evidence, gate outcomes, and the passing hardcoded-secret scan
|
||||||
|
</acceptance_criteria>
|
||||||
|
<verify>
|
||||||
|
<automated>docker compose config --quiet</automated>
|
||||||
|
</verify>
|
||||||
|
<done>Phase 13 documentation, version metadata, and closeout evidence are ready for final validation.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Run the full test, dependency, security, and hardcoded-secret gates before commit and push</name>
|
||||||
|
<files>.planning/ROADMAP.md, AGENTS.md, README.md, RUNBOOK.md, SECURITY.md, backend/main.py, frontend/package.json</files>
|
||||||
|
<read_first>
|
||||||
|
- AGENTS.md
|
||||||
|
- SECURITY.md
|
||||||
|
- .planning/phases/13-virtual-local-cloud-operations/13-VALIDATION.md
|
||||||
|
</read_first>
|
||||||
|
<action>
|
||||||
|
Run the full backend and frontend suites, then the focused security and dependency gates required by AGENTS.md. Every backend pytest invocation must be a direct `docker compose run --rm backend pytest ...` command. Run `docker compose run --rm backend bandit -r .`, `docker compose run --rm backend pip audit`, `cd frontend && npm audit --audit-level=high`, and the non-skippable hardcoded-secret scan `trufflehog filesystem --no-update --fail .`. Confirm the wrong-owner, admin-negative, credential-secrecy, SSRF, typed conflict, audit secrecy, and no-probe-on-navigation invariants remain green. Review the final diff for unrelated files or secret exposure, then stage, commit, and push atomically as the closeout action for the completed phase.
|
||||||
|
</action>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- full backend and frontend suites pass
|
||||||
|
- backend security and dependency gates pass with direct containerized commands
|
||||||
|
- `trufflehog filesystem --no-update --fail .` passes and is recorded as closeout evidence
|
||||||
|
- the phase can be committed and pushed without unrelated files or secret exposure
|
||||||
|
</acceptance_criteria>
|
||||||
|
<verify>
|
||||||
|
<automated>docker compose run --rm backend pytest -v</automated>
|
||||||
|
<automated>cd frontend && npm run test</automated>
|
||||||
|
<automated>docker compose run --rm backend bandit -r .</automated>
|
||||||
|
<automated>docker compose run --rm backend pip audit</automated>
|
||||||
|
<automated>cd frontend && npm audit --audit-level=high</automated>
|
||||||
|
<automated>trufflehog filesystem --no-update --fail .</automated>
|
||||||
|
<automated>git diff --check</automated>
|
||||||
|
</verify>
|
||||||
|
<done>Phase 13 is fully validated, secret-scanned, and ready for atomic commit and push.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<threat_model>
|
||||||
|
## Trust Boundaries
|
||||||
|
|
||||||
|
| Boundary | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| shipped code → documentation/security evidence | Closeout must describe only what actually shipped. |
|
||||||
|
| final diff → git history | Secret exposure and unrelated changes must be caught before commit. |
|
||||||
|
|
||||||
|
## STRIDE Threat Register
|
||||||
|
|
||||||
|
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||||
|
|-----------|----------|-----------|-------------|-----------------|
|
||||||
|
| T-13-33 | R | closeout docs | mitigate | Final-plan-only doc update ensures documentation follows completed implementation. |
|
||||||
|
| T-13-34 | I | release validation | mitigate | Full suites, dependency audits, and the non-skippable `trufflehog filesystem --no-update --fail .` gate run before commit and push. |
|
||||||
|
</threat_model>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- Confirm all backend pytest commands are direct containerized invocations.
|
||||||
|
- Confirm full backend, frontend, security, dependency, and hardcoded-secret gates are isolated to this plan.
|
||||||
|
- Confirm docs and versions match the implemented Phase 13 scope and nothing deferred is claimed as shipped.
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- Docs, versions, security evidence, full gates, and the explicit hardcoded-secret scan are isolated to one bounded plan.
|
||||||
|
- Phase 13 can be closed without mixing implementation work into the same plan.
|
||||||
|
- The final plan preserves the project’s commit, push, documentation, and closeout protocol exactly.
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
Create `.planning/phases/13-virtual-local-cloud-operations/13-11-SUMMARY.md` when done
|
||||||
|
</output>
|
||||||
@@ -37,6 +37,10 @@ Deliver owner-authorized connection maintenance and the main cloud file-manageme
|
|||||||
- **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-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.
|
- **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.
|
||||||
|
|
||||||
|
### Provider access and preview scope
|
||||||
|
- **D-17:** Phase 13 requests broader Google Drive access rather than retaining the restricted `drive.file` scope, so authorized users can operate on existing Drive items throughout their connected storage. Consent copy and tests must make the expanded scope explicit.
|
||||||
|
- **D-18:** Phase 13 preview support is limited to supported binary file formats. Google Workspace and Microsoft Office document rendering/editing are excluded; unsupported formats use the ownership-checked authorized download fallback from D-02.
|
||||||
|
|
||||||
### Codex's Discretion
|
### Codex's Discretion
|
||||||
- Choose exact accessible dialog wording, progress labels, icons, and controlled provider-error translations while preserving the decisions above.
|
- Choose exact accessible dialog wording, progress labels, icons, and controlled provider-error translations while preserving the decisions above.
|
||||||
- Choose the bounded retry count for automatic collision suffixing and safe provider-specific mechanics for trash versus permanent delete.
|
- Choose the bounded retry count for automatic collision suffixing and safe provider-specific mechanics for trash versus permanent delete.
|
||||||
@@ -119,6 +123,7 @@ Deliver owner-authorized connection maintenance and the main cloud file-manageme
|
|||||||
## Deferred Ideas
|
## Deferred Ideas
|
||||||
|
|
||||||
- Phase 14 must place bytes used for in-app cloud preview into DocuVault's temporary cache and remove them through normal cache eviction/clearing; preview must not become a browser-to-device download.
|
- Phase 14 must place bytes used for in-app cloud preview into DocuVault's temporary cache and remove them through normal cache eviction/clearing; preview must not become a browser-to-device download.
|
||||||
|
- A future phase will add Office document rendering/editing through Collabora running in a separate internally accessible container; Phase 13 must not introduce Collabora, Google Workspace export preview, or Office-native preview plumbing.
|
||||||
- Future `IMPORT-01`: on explicit disconnect, convert cached/downloaded cloud files into local DocuVault documents under a folder named after the connection's current display name, preserve the cloud hierarchy, continue counting those bytes toward quota, and leave provider files untouched. This is a permanent import capability and remains outside Phase 13 and the current v0.3 scope.
|
- Future `IMPORT-01`: on explicit disconnect, convert cached/downloaded cloud files into local DocuVault documents under a folder named after the connection's current display name, preserve the cloud hierarchy, continue counting those bytes toward quota, and leave provider files untouched. This is a permanent import capability and remains outside Phase 13 and the current v0.3 scope.
|
||||||
|
|
||||||
</deferred>
|
</deferred>
|
||||||
|
|||||||
@@ -0,0 +1,559 @@
|
|||||||
|
# Phase 13: Virtual-Local Cloud Operations - Pattern Map
|
||||||
|
|
||||||
|
**Mapped:** 2026-06-22
|
||||||
|
**Files analyzed:** 33 likely new/modified files
|
||||||
|
**Analogs found:** 33 / 33 (6 are composite/no-exact)
|
||||||
|
|
||||||
|
## Inherited phase rules that stay locked
|
||||||
|
|
||||||
|
- Keep `StorageBrowser.vue` as the only file browser. Cloud behavior extends emitted events and props; it does not fork layout. See [AGENTS.md], Phase 12 patterns, and [frontend/src/components/storage/StorageBrowser.vue].
|
||||||
|
- Keep cloud routing owner-scoped by connection UUID plus opaque `provider_item_id`; never parse provider refs on the client. Primary analogs: [backend/api/cloud/browse.py:183], [frontend/src/views/CloudFolderView.vue:42], [frontend/src/components/cloud/CloudFolderTreeItem.vue:42].
|
||||||
|
- Keep metadata reconciliation centralized in `reconcile_cloud_listing` / `apply_listing_and_finalize`; provider adapters never write `cloud_items` rows directly. Primary analogs: [backend/services/cloud_items.py:163], [backend/services/cloud_items.py:271].
|
||||||
|
- Keep service-layer exception discipline: services raise domain errors / `ValueError`, routers translate to `HTTPException`. Primary analogs: [AGENTS.md], [backend/services/cloud_items.py:1].
|
||||||
|
- Keep audit writes metadata-only and in the caller’s transaction. Primary analog: [backend/services/audit.py:27].
|
||||||
|
|
||||||
|
## File Classification
|
||||||
|
|
||||||
|
| Likely file | Role | Data flow | Closest analog | Match |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `backend/api/cloud/operations.py` | route | request-response + file-I/O + CRUD | `backend/api/cloud/browse.py`, `backend/api/documents/content.py`, `backend/api/documents/upload.py`, `backend/api/folders.py` | composite |
|
||||||
|
| `backend/api/cloud/connections.py` | route | request-response | itself + `backend/tasks/cloud_tasks.py` | exact |
|
||||||
|
| `backend/api/cloud/schemas.py` | schema | transform | itself | exact |
|
||||||
|
| `backend/services/cloud_operations.py` | service | CRUD + transform | `backend/services/cloud_items.py` + `backend/services/audit.py` | composite |
|
||||||
|
| `backend/services/cloud_items.py` | service | CRUD + batch reconcile | itself | exact |
|
||||||
|
| `backend/storage/cloud_base.py` | contract | transform | itself | exact |
|
||||||
|
| `backend/storage/cloud_backend_factory.py` | factory | request-response | itself | exact |
|
||||||
|
| `backend/storage/google_drive_backend.py` | provider | file-I/O + CRUD | itself | exact |
|
||||||
|
| `backend/storage/onedrive_backend.py` | provider | file-I/O + CRUD | itself | exact |
|
||||||
|
| `backend/storage/webdav_backend.py` | provider | file-I/O + CRUD | itself | exact |
|
||||||
|
| `backend/storage/nextcloud_backend.py` | provider | file-I/O | itself + `webdav_backend.py` | exact |
|
||||||
|
| `backend/tasks/cloud_tasks.py` | task | event-driven + batch | itself | exact |
|
||||||
|
| `frontend/src/api/cloud.js` | API client | request-response | itself + `frontend/src/api/documents.js` | exact |
|
||||||
|
| `frontend/src/stores/cloudConnections.js` | store | state transform | itself | exact |
|
||||||
|
| `frontend/src/views/CloudFolderView.vue` | thin view | request-response + queue orchestration | itself + `frontend/src/views/FileManagerView.vue` | exact |
|
||||||
|
| `frontend/src/components/storage/StorageBrowser.vue` | smart component | CRUD UI + queue UI | itself | exact |
|
||||||
|
| `frontend/src/components/settings/SettingsCloudTab.vue` | smart component | request-response | itself | exact |
|
||||||
|
| `frontend/src/components/cloud/CloudCredentialModal.vue` | modal/form | request-response | itself | exact |
|
||||||
|
| `backend/tests/test_cloud.py` | integration test | request-response | itself | exact |
|
||||||
|
| `backend/tests/test_cloud_security.py` | security-negative test | request-response | itself | exact |
|
||||||
|
| `backend/tests/test_cloud_backends.py` | provider unit/contract test | file-I/O | itself | exact |
|
||||||
|
| `backend/tests/test_cloud_provider_contract.py` | provider contract test | transform | itself | exact |
|
||||||
|
| `backend/tests/test_cloud_items.py` | service/task test | CRUD + batch | itself | exact |
|
||||||
|
| `backend/tests/test_cloud_mutations.py` | integration/contract test | CRUD + file-I/O | `test_cloud.py` + `test_cloud_security.py` + `test_cloud_items.py` | composite |
|
||||||
|
| `backend/tests/test_cloud_reconnect.py` | integration/security test | request-response + task | `test_cloud.py` + `test_cloud_items.py` | composite |
|
||||||
|
| `backend/tests/test_cloud_audit.py` | integration test | transform | `backend/tests/test_audit.py` + `backend/tests/test_cloud.py` | partial |
|
||||||
|
| `frontend/src/views/__tests__/CloudFolderView.test.js` | view unit test | request-response | itself | exact |
|
||||||
|
| `frontend/src/views/__tests__/CloudFolderRenderedFlow.test.js` | rendered-flow test | request-response | itself | exact |
|
||||||
|
| `frontend/src/components/storage/__tests__/StorageBrowser.capabilities.test.js` | component unit test | CRUD UI | itself | exact |
|
||||||
|
| `frontend/src/components/storage/__tests__/StorageBrowser.cloud-queue.test.js` | component unit test | queue UI | `StorageBrowser.capabilities.test.js` + `FileManagerView.vue` upload flow | composite |
|
||||||
|
| `frontend/src/views/__tests__/CloudFolderOpenPreview.test.js` | rendered-flow test | request-response + preview | `CloudFolderRenderedFlow.test.js` + document content helpers | composite |
|
||||||
|
| `frontend/src/stores/__tests__/cloudConnections.test.js` | store unit test | state transform | itself | exact |
|
||||||
|
| `frontend/src/components/settings/__tests__/SettingsCloudTab.test.js` | component unit test | request-response | itself | exact |
|
||||||
|
| `frontend/src/components/settings/__tests__/SettingsCloudTab.health.test.js` | component unit test | health/reconnect UI | `SettingsCloudTab.test.js` + store freshness tests | partial |
|
||||||
|
|
||||||
|
## Pattern Assignments
|
||||||
|
|
||||||
|
### `backend/api/cloud/operations.py`
|
||||||
|
|
||||||
|
**Primary analogs**
|
||||||
|
|
||||||
|
- Route/dependency skeleton: [backend/api/cloud/browse.py:19-42], [backend/api/cloud/browse.py:183-214]
|
||||||
|
- Connection settings/test/update/disconnect patterns: [backend/api/cloud/connections.py:368-434], [backend/api/cloud/connections.py:492-579], [backend/api/cloud/connections.py:604-635]
|
||||||
|
- Authorized byte streaming and cloud-error translation: [backend/api/documents/content.py:50-103]
|
||||||
|
- Multipart upload proxy pattern: [backend/api/documents/upload.py:122-198]
|
||||||
|
- Local create/rename/delete/move handler shape: [backend/api/folders.py:122-160], [backend/api/folders.py:232-265], [backend/api/folders.py:268-351], [backend/api/folders.py:357-375]
|
||||||
|
|
||||||
|
**Copy these conventions**
|
||||||
|
|
||||||
|
- Use `get_regular_user`, `get_db`, `account_limiter`, and `request.state.current_user = current_user` exactly like cloud browse/connections.
|
||||||
|
- Resolve ownership through `resolve_owned_connection(...)` before any provider action.
|
||||||
|
- Translate provider/service failures to controlled `HTTPException` messages like document content/upload already do; do not surface raw provider text.
|
||||||
|
- For open/preview/download, copy the `StreamingResponse` + `CloudConnectionError` mapping from `content.py`, but key authorization off cloud connection/item ownership instead of `Document`.
|
||||||
|
- For upload/create/rename/move/delete, keep router bodies thin: validate request shape, call service, return whitelisted schema, write no inline reconciliation logic.
|
||||||
|
|
||||||
|
**No exact analog**
|
||||||
|
|
||||||
|
- There is no existing single cloud route file that combines owner-scoped connection actions with content proxying and normalized mutation results. Keep the file shaped like `browse.py`, but borrow handler bodies from the document/folder routers above.
|
||||||
|
|
||||||
|
### `backend/api/cloud/connections.py`
|
||||||
|
|
||||||
|
**Primary analogs**
|
||||||
|
|
||||||
|
- WebDAV connect and health-check flow: [backend/api/cloud/connections.py:368-434]
|
||||||
|
- Credential update flow: [backend/api/cloud/connections.py:492-579]
|
||||||
|
- Display-name rename: [backend/api/cloud/connections.py:582-601]
|
||||||
|
- Disconnect confirmation target: [backend/api/cloud/connections.py:604-635]
|
||||||
|
|
||||||
|
**Copy these conventions**
|
||||||
|
|
||||||
|
- Keep `_get_owned_connection(...)` for owner checks.
|
||||||
|
- Keep URL validation before backend construction.
|
||||||
|
- Keep audit writes adjacent to the successful state change.
|
||||||
|
- If reconnect/test routes are added, model them after `connect_webdav` + `update_webdav_credentials`: validate, health-check, persist, audit, commit.
|
||||||
|
|
||||||
|
**Phase 13-specific caution**
|
||||||
|
|
||||||
|
- The current OAuth callback always inserts a new row (`_upsert_cloud_connection = _insert_cloud_connection`) at [backend/api/cloud/connections.py:128-129]. Do not copy that behavior for reconnect; Phase 13 needs connection-row preservation.
|
||||||
|
|
||||||
|
### `backend/api/cloud/schemas.py`
|
||||||
|
|
||||||
|
**Primary analog**
|
||||||
|
|
||||||
|
- Current cloud whitelist schemas: [backend/api/cloud/schemas.py:17-86]
|
||||||
|
|
||||||
|
**Copy these conventions**
|
||||||
|
|
||||||
|
- Add only explicit allowlist response/request models.
|
||||||
|
- Keep validators at schema level for user-submitted fields, as in `ConnectionRenameRequest`.
|
||||||
|
- Keep credentials, provider URLs, and raw provider payloads out of every schema.
|
||||||
|
|
||||||
|
**No exact analog**
|
||||||
|
|
||||||
|
- There is no current normalized mutation-result schema. Add new result types beside `CloudBrowseResponse`, not in routers.
|
||||||
|
|
||||||
|
### `backend/services/cloud_operations.py`
|
||||||
|
|
||||||
|
**Primary analogs**
|
||||||
|
|
||||||
|
- Owner resolution / stable identity / folder freshness: [backend/services/cloud_items.py:38-62], [backend/services/cloud_items.py:97-159], [backend/services/cloud_items.py:271-342]
|
||||||
|
- Metadata-only audit helper: [backend/services/audit.py:27-58]
|
||||||
|
- Background refresh orchestration: [backend/tasks/cloud_tasks.py:50-163]
|
||||||
|
|
||||||
|
**Copy these conventions**
|
||||||
|
|
||||||
|
- Accept UUIDs/strings the same way `cloud_items.py` does.
|
||||||
|
- Raise domain errors or `ValueError`, never `HTTPException`.
|
||||||
|
- Reconcile and finalize folder state before returning success.
|
||||||
|
- Write audit rows through `write_audit_log(...)`, letting the caller own the transaction.
|
||||||
|
|
||||||
|
**No exact analog**
|
||||||
|
|
||||||
|
- There is no existing service that turns provider mutation outcomes into normalized API results. Treat this as the new orchestration seam; do not push that logic into routers or providers.
|
||||||
|
|
||||||
|
### `backend/services/cloud_items.py`
|
||||||
|
|
||||||
|
**Primary analog**
|
||||||
|
|
||||||
|
- Keep using the current service itself.
|
||||||
|
|
||||||
|
**Copy these conventions**
|
||||||
|
|
||||||
|
- Owner resolution: [backend/services/cloud_items.py:38-62]
|
||||||
|
- Stable upsert-by-`(connection_id, provider_item_id)`: [backend/services/cloud_items.py:97-159]
|
||||||
|
- Complete-vs-incomplete reconciliation: [backend/services/cloud_items.py:163-213]
|
||||||
|
- Single freshness gate: [backend/services/cloud_items.py:271-342]
|
||||||
|
- Controlled folder-state updates: [backend/services/cloud_items.py:345-384]
|
||||||
|
|
||||||
|
**Planner note**
|
||||||
|
|
||||||
|
- Any mutation service should feed its authoritative provider result back through these primitives instead of adding direct ORM writes elsewhere.
|
||||||
|
|
||||||
|
### `backend/storage/cloud_base.py`
|
||||||
|
|
||||||
|
**Primary analog**
|
||||||
|
|
||||||
|
- Keep extending the current contract file itself: [backend/storage/cloud_base.py:27-245]
|
||||||
|
|
||||||
|
**Copy these conventions**
|
||||||
|
|
||||||
|
- Stable action vocabulary and reason codes live here, not in routers or Vue.
|
||||||
|
- Immutable normalized dataclasses (`CloudCapability`, `CloudResource`, `CloudListing`) are the pattern to follow for new mutation result types.
|
||||||
|
- Keep provider-specific branching out of the shared contract.
|
||||||
|
|
||||||
|
**No exact analog**
|
||||||
|
|
||||||
|
- There is no Phase-13 mutation result/value type yet. New normalized result dataclasses belong here.
|
||||||
|
|
||||||
|
### `backend/storage/cloud_backend_factory.py`
|
||||||
|
|
||||||
|
**Primary analog**
|
||||||
|
|
||||||
|
- Current factory and adapter assertion: [backend/storage/cloud_backend_factory.py:7-66]
|
||||||
|
|
||||||
|
**Copy these conventions**
|
||||||
|
|
||||||
|
- Keep lazy imports.
|
||||||
|
- Keep Nextcloud URL normalization in the factory boundary.
|
||||||
|
- If a mutable-adapter subclass is introduced, assert the interface here instead of switching on provider names in routers.
|
||||||
|
|
||||||
|
### `backend/storage/google_drive_backend.py`
|
||||||
|
|
||||||
|
**Primary analog**
|
||||||
|
|
||||||
|
- Existing upload/download/delete/list/capability patterns: [backend/storage/google_drive_backend.py:140-172], [backend/storage/google_drive_backend.py:174-207], [backend/storage/google_drive_backend.py:257-272], [backend/storage/google_drive_backend.py:276-417]
|
||||||
|
|
||||||
|
**Copy these conventions**
|
||||||
|
|
||||||
|
- Wrap SDK calls in `asyncio.to_thread()`.
|
||||||
|
- Centralize provider error classification in helper methods like `_handle_http_error(...)`.
|
||||||
|
- Normalize provider metadata into `CloudResource` objects.
|
||||||
|
- Keep the backend stateless: it may return refreshed credentials or typed errors, but must not write DB state directly.
|
||||||
|
|
||||||
|
### `backend/storage/onedrive_backend.py`
|
||||||
|
|
||||||
|
**Primary analog**
|
||||||
|
|
||||||
|
- Existing token lifecycle + upload/download/delete/list/capabilities: [backend/storage/onedrive_backend.py:83-154], [backend/storage/onedrive_backend.py:158-218], [backend/storage/onedrive_backend.py:220-245], [backend/storage/onedrive_backend.py:280-409]
|
||||||
|
|
||||||
|
**Copy these conventions**
|
||||||
|
|
||||||
|
- Refresh token on demand inside the backend boundary.
|
||||||
|
- Keep Graph HTTP async with `httpx`.
|
||||||
|
- Keep `CloudConnectionError` unified across providers.
|
||||||
|
|
||||||
|
**Gap to flag**
|
||||||
|
|
||||||
|
- There is no persistence pattern yet for refreshed OneDrive credentials; Phase 13 must add one above the backend, not inside it.
|
||||||
|
|
||||||
|
### `backend/storage/webdav_backend.py`
|
||||||
|
|
||||||
|
**Primary analog**
|
||||||
|
|
||||||
|
- Existing SSRF, PUT/GET/DELETE, list-folder, and capability patterns: [backend/storage/webdav_backend.py:66-137], [backend/storage/webdav_backend.py:139-174], [backend/storage/webdav_backend.py:222-237], [backend/storage/webdav_backend.py:241-370]
|
||||||
|
|
||||||
|
**Copy these conventions**
|
||||||
|
|
||||||
|
- Re-run `validate_cloud_url(...)` before every outbound request.
|
||||||
|
- Keep basename/path-traversal protection when building destination names.
|
||||||
|
- Keep WebDAV-specific overwrite/conflict semantics inside this provider file.
|
||||||
|
|
||||||
|
### `backend/storage/nextcloud_backend.py`
|
||||||
|
|
||||||
|
**Primary analog**
|
||||||
|
|
||||||
|
- Minimal specialization over WebDAV: [backend/storage/nextcloud_backend.py:38-90]
|
||||||
|
|
||||||
|
**Copy these conventions**
|
||||||
|
|
||||||
|
- Keep Nextcloud as a tiny specialization, not a parallel backend.
|
||||||
|
- If Phase 13 needs Nextcloud-specific mutation quirks, implement them as narrow overrides that preserve the public contract.
|
||||||
|
|
||||||
|
### `backend/tasks/cloud_tasks.py`
|
||||||
|
|
||||||
|
**Primary analog**
|
||||||
|
|
||||||
|
- Current refresh worker: [backend/tasks/cloud_tasks.py:50-163], [backend/tasks/cloud_tasks.py:175-216]
|
||||||
|
|
||||||
|
**Copy these conventions**
|
||||||
|
|
||||||
|
- Revalidate ownership inside the worker.
|
||||||
|
- Decrypt credentials inside the worker, never in broker payload.
|
||||||
|
- Use sentinel exceptions to separate retryable provider failures from terminal auth failures.
|
||||||
|
- Reuse this task model if reconnect or stale-metadata recovery needs post-success refresh.
|
||||||
|
|
||||||
|
### `frontend/src/api/cloud.js`
|
||||||
|
|
||||||
|
**Primary analogs**
|
||||||
|
|
||||||
|
- Existing connection browse/config/update methods: [frontend/src/api/cloud.js:11-18], [frontend/src/api/cloud.js:36-50], [frontend/src/api/cloud.js:73-88]
|
||||||
|
- Cloud upload helper shape: [frontend/src/api/documents.js:48-54]
|
||||||
|
|
||||||
|
**Copy these conventions**
|
||||||
|
|
||||||
|
- Keep all Phase 13 cloud endpoints behind this domain module.
|
||||||
|
- Keep connection UUID and encoded `parent_ref` handling here.
|
||||||
|
- Add new `open/preview/upload/create/rename/move/delete/test/reconnect` helpers here instead of calling raw URLs from views/components.
|
||||||
|
|
||||||
|
### `frontend/src/stores/cloudConnections.js`
|
||||||
|
|
||||||
|
**Primary analog**
|
||||||
|
|
||||||
|
- Current store state and reset/mapping helpers: [frontend/src/stores/cloudConnections.js:33-138]
|
||||||
|
|
||||||
|
**Copy these conventions**
|
||||||
|
|
||||||
|
- Keep server state translation centralized in the store.
|
||||||
|
- Keep sessionStorage limited to folder refs only.
|
||||||
|
- Reset browse state on connection switch.
|
||||||
|
|
||||||
|
**No exact analog**
|
||||||
|
|
||||||
|
- There is no queue/conflict/resume state machine yet. Add it here only if it is shared across cloud views/settings; otherwise keep queue orchestration in `CloudFolderView`.
|
||||||
|
|
||||||
|
### `frontend/src/views/CloudFolderView.vue`
|
||||||
|
|
||||||
|
**Primary analogs**
|
||||||
|
|
||||||
|
- Existing cloud thin-view contract: [frontend/src/views/CloudFolderView.vue:1-20], [frontend/src/views/CloudFolderView.vue:125-170]
|
||||||
|
- Local upload / create / rename / move / delete orchestration: [frontend/src/views/FileManagerView.vue:1-30], [frontend/src/views/FileManagerView.vue:103-182]
|
||||||
|
|
||||||
|
**Copy these conventions**
|
||||||
|
|
||||||
|
- Keep the view thin: store/router/API orchestration only.
|
||||||
|
- Pass everything into `StorageBrowser`; do not add layout or grid logic here.
|
||||||
|
- Reuse the local upload queue orchestration style from `FileManagerView`, but Phase 13 needs sequential pause/resume instead of the current all-at-once `Promise.allSettled(...)` placeholder at [frontend/src/views/CloudFolderView.vue:172-181].
|
||||||
|
- Use toasts and success/error handling the same way `FileManagerView` does.
|
||||||
|
|
||||||
|
### `frontend/src/components/storage/StorageBrowser.vue`
|
||||||
|
|
||||||
|
**Primary analog**
|
||||||
|
|
||||||
|
- Extend the current shared component itself: [frontend/src/components/storage/StorageBrowser.vue:140-275], [frontend/src/components/storage/StorageBrowser.vue:424-681]
|
||||||
|
|
||||||
|
**Copy these conventions**
|
||||||
|
|
||||||
|
- Props/events are the contract; keep new behavior behind emitted events and prop-driven state.
|
||||||
|
- Capability-aware buttons and notices stay in this component: [frontend/src/components/storage/StorageBrowser.vue:360-418], [frontend/src/components/storage/StorageBrowser.vue:495-530].
|
||||||
|
- New folder / rename inline patterns already exist here: [frontend/src/components/storage/StorageBrowser.vue:118-135], [frontend/src/components/storage/StorageBrowser.vue:157-166], [frontend/src/components/storage/StorageBrowser.vue:543-593].
|
||||||
|
- File move picker and drag-to-move patterns already exist here: [frontend/src/components/storage/StorageBrowser.vue:253-344], [frontend/src/components/storage/StorageBrowser.vue:595-621].
|
||||||
|
|
||||||
|
**Gap to flag**
|
||||||
|
|
||||||
|
- There is no existing conflict dialog / paused upload queue UI. That is a real no-analog area inside the shared browser and should be added here rather than in a cloud-only component.
|
||||||
|
|
||||||
|
### `frontend/src/components/settings/SettingsCloudTab.vue`
|
||||||
|
|
||||||
|
**Primary analog**
|
||||||
|
|
||||||
|
- Existing settings actions and confirm flows: [frontend/src/components/settings/SettingsCloudTab.vue:21-102], [frontend/src/components/settings/SettingsCloudTab.vue:104-170], [frontend/src/components/settings/SettingsCloudTab.vue:293-359]
|
||||||
|
|
||||||
|
**Copy these conventions**
|
||||||
|
|
||||||
|
- Keep provider rows, inline status badges, and destructive confirms in one shared settings tab.
|
||||||
|
- Add Test/Reconnect controls beside existing Connect/Edit/Remove actions; do not create a parallel health screen.
|
||||||
|
- Keep OAuth initiation in the component and store mutation calls behind the store/API layer.
|
||||||
|
|
||||||
|
### `frontend/src/components/cloud/CloudCredentialModal.vue`
|
||||||
|
|
||||||
|
**Primary analog**
|
||||||
|
|
||||||
|
- Existing submit branching for create vs edit: [frontend/src/components/cloud/CloudCredentialModal.vue:301-323]
|
||||||
|
|
||||||
|
**Copy these conventions**
|
||||||
|
|
||||||
|
- Keep credential edits inside the modal for WebDAV/Nextcloud.
|
||||||
|
- If Phase 13 adds explicit “Test connection” before save, hang it off the same API module and modal state machine instead of duplicating form state elsewhere.
|
||||||
|
|
||||||
|
### `backend/tests/test_cloud.py`
|
||||||
|
|
||||||
|
**Primary analog**
|
||||||
|
|
||||||
|
- Current integration coverage for connect/upload/status/browse/rename/refresh: [backend/tests/test_cloud.py:30-65], [backend/tests/test_cloud.py:473-527], [backend/tests/test_cloud.py:532-655], [backend/tests/test_cloud.py:998-1315]
|
||||||
|
|
||||||
|
**Copy these conventions**
|
||||||
|
|
||||||
|
- Use the shared auth helper pattern and `async_client`.
|
||||||
|
- Patch provider/network boundaries, not route internals, where possible.
|
||||||
|
- Assert both HTTP contract and DB side effects.
|
||||||
|
|
||||||
|
### `backend/tests/test_cloud_security.py`
|
||||||
|
|
||||||
|
**Primary analog**
|
||||||
|
|
||||||
|
- Current security-negative suite: [backend/tests/test_cloud_security.py:37-117], [backend/tests/test_cloud_security.py:122-340]
|
||||||
|
|
||||||
|
**Copy these conventions**
|
||||||
|
|
||||||
|
- Add Phase 13 wrong-owner/admin/credential/no-byte/cross-connection negatives here.
|
||||||
|
- Keep raw-provider-error sanitization assertions here, not only in happy-path integration tests.
|
||||||
|
|
||||||
|
### `backend/tests/test_cloud_backends.py`
|
||||||
|
|
||||||
|
**Primary analog**
|
||||||
|
|
||||||
|
- Provider backend behavior tests: [backend/tests/test_cloud_backends.py:334-420] and the rest of the file’s provider sections
|
||||||
|
|
||||||
|
**Copy these conventions**
|
||||||
|
|
||||||
|
- Provider-specific fixtures, normalized assertions.
|
||||||
|
- Explicit “never download/mutate while browsing” checks.
|
||||||
|
- Keep backend tests provider-neutral where possible and provider-specific only where semantics differ.
|
||||||
|
|
||||||
|
### `backend/tests/test_cloud_provider_contract.py`
|
||||||
|
|
||||||
|
**Primary analog**
|
||||||
|
|
||||||
|
- Canonical signature/identity/pagination/no-byte contract: [backend/tests/test_cloud_provider_contract.py:201-276], [backend/tests/test_cloud_provider_contract.py:376-536], [backend/tests/test_cloud_provider_contract.py:659-727]
|
||||||
|
|
||||||
|
**Copy these conventions**
|
||||||
|
|
||||||
|
- Any new mutable adapter methods should get the same style of canonical contract tests: signature, normalized result, trusted caller identity, no forbidden side effects.
|
||||||
|
|
||||||
|
### `backend/tests/test_cloud_items.py`
|
||||||
|
|
||||||
|
**Primary analog**
|
||||||
|
|
||||||
|
- Owner-scoped service and folder-state tests: [backend/tests/test_cloud_items.py:368-529], [backend/tests/test_cloud_items.py:545-859]
|
||||||
|
|
||||||
|
**Copy these conventions**
|
||||||
|
|
||||||
|
- Mutation reconciliation tests belong here: stable UUID across rename/move, complete/incomplete behavior, freshness truth, no quota mutation.
|
||||||
|
|
||||||
|
### `backend/tests/test_cloud_mutations.py`
|
||||||
|
|
||||||
|
**Closest analogs**
|
||||||
|
|
||||||
|
- API behavior: [backend/tests/test_cloud.py:473-527], [backend/tests/test_cloud.py:1112-1249]
|
||||||
|
- Security negatives: [backend/tests/test_cloud_security.py:122-340]
|
||||||
|
- Reconciliation truth: [backend/tests/test_cloud_items.py:411-499], [backend/tests/test_cloud_items.py:743-859]
|
||||||
|
|
||||||
|
**No exact analog**
|
||||||
|
|
||||||
|
- There is no existing end-to-end mutation suite for cloud items. Build it as a new file, but follow the helper/fixture style of `test_cloud.py`.
|
||||||
|
|
||||||
|
### `backend/tests/test_cloud_reconnect.py`
|
||||||
|
|
||||||
|
**Closest analogs**
|
||||||
|
|
||||||
|
- Connection lifecycle/status: [backend/tests/test_cloud.py:532-655]
|
||||||
|
- Worker/freshness behavior: [backend/tests/test_cloud.py:1251-1315], [backend/tests/test_cloud_items.py:545-609]
|
||||||
|
|
||||||
|
**No exact analog**
|
||||||
|
|
||||||
|
- There is no dedicated reconnect/token-persistence suite today.
|
||||||
|
|
||||||
|
### `backend/tests/test_cloud_audit.py`
|
||||||
|
|
||||||
|
**Closest analogs**
|
||||||
|
|
||||||
|
- Audit helper behavior: [backend/services/audit.py:27-58]
|
||||||
|
- Inline audit assertions in cloud routes: [backend/api/cloud/connections.py:351-359], [backend/api/cloud/connections.py:422-430], [backend/api/cloud/connections.py:567-575], [backend/api/cloud/connections.py:624-632]
|
||||||
|
|
||||||
|
**No exact analog**
|
||||||
|
|
||||||
|
- There is no cloud-mutation audit suite today; create one that asserts metadata-only payloads and same-transaction persistence.
|
||||||
|
|
||||||
|
### `frontend/src/views/__tests__/CloudFolderView.test.js`
|
||||||
|
|
||||||
|
**Primary analog**
|
||||||
|
|
||||||
|
- Existing thin-view cloud tests: [frontend/src/views/__tests__/CloudFolderView.test.js:73-294]
|
||||||
|
|
||||||
|
**Copy these conventions**
|
||||||
|
|
||||||
|
- Stub `StorageBrowser` when the test is about view orchestration only.
|
||||||
|
- Assert route params, API calls, and props passed to the browser.
|
||||||
|
|
||||||
|
### `frontend/src/views/__tests__/CloudFolderRenderedFlow.test.js`
|
||||||
|
|
||||||
|
**Primary analog**
|
||||||
|
|
||||||
|
- Existing real-browser rendered flow: [frontend/src/views/__tests__/CloudFolderRenderedFlow.test.js:1-260]
|
||||||
|
|
||||||
|
**Copy these conventions**
|
||||||
|
|
||||||
|
- Render the real `StorageBrowser` when testing end-to-end view/browser interactions.
|
||||||
|
- Keep only router/API boundaries mocked.
|
||||||
|
|
||||||
|
### `frontend/src/components/storage/__tests__/StorageBrowser.capabilities.test.js`
|
||||||
|
|
||||||
|
**Primary analog**
|
||||||
|
|
||||||
|
- Current accessibility and unsupported-capability suite: [frontend/src/components/storage/__tests__/StorageBrowser.capabilities.test.js:47-257]
|
||||||
|
|
||||||
|
**Copy these conventions**
|
||||||
|
|
||||||
|
- Assert emitted events, `aria-disabled`, notices, and touch-target classes from the real component.
|
||||||
|
- Extend this file for new capability buttons only; do not hide cloud actions based on mode.
|
||||||
|
|
||||||
|
### `frontend/src/components/storage/__tests__/StorageBrowser.cloud-queue.test.js`
|
||||||
|
|
||||||
|
**Closest analogs**
|
||||||
|
|
||||||
|
- Capability interaction assertions: [frontend/src/components/storage/__tests__/StorageBrowser.capabilities.test.js:89-190]
|
||||||
|
- Local upload queue behavior source: [frontend/src/views/FileManagerView.vue:103-128]
|
||||||
|
|
||||||
|
**No exact analog**
|
||||||
|
|
||||||
|
- There is no existing paused sequential queue test file. Create it as a dedicated component suite around the shared browser’s new queue/conflict UI.
|
||||||
|
|
||||||
|
### `frontend/src/views/__tests__/CloudFolderOpenPreview.test.js`
|
||||||
|
|
||||||
|
**Closest analogs**
|
||||||
|
|
||||||
|
- Rendered cloud flow: [frontend/src/views/__tests__/CloudFolderRenderedFlow.test.js:170-260]
|
||||||
|
- Authorized document content fetch helper: [frontend/src/api/documents.js:60-81]
|
||||||
|
|
||||||
|
**No exact analog**
|
||||||
|
|
||||||
|
- There is no cloud open/preview rendered-flow test today.
|
||||||
|
|
||||||
|
### `frontend/src/stores/__tests__/cloudConnections.test.js`
|
||||||
|
|
||||||
|
**Primary analog**
|
||||||
|
|
||||||
|
- Existing store tests for selection, freshness, session state: [frontend/src/stores/__tests__/cloudConnections.test.js:28-237]
|
||||||
|
|
||||||
|
**Copy these conventions**
|
||||||
|
|
||||||
|
- Keep pure store tests focused on mapping and reset behavior.
|
||||||
|
- If queue state or health-state translation lives in the store, test it here once.
|
||||||
|
|
||||||
|
### `frontend/src/components/settings/__tests__/SettingsCloudTab.test.js`
|
||||||
|
|
||||||
|
**Primary analog**
|
||||||
|
|
||||||
|
- Existing settings-tab tests: [frontend/src/components/settings/__tests__/SettingsCloudTab.test.js:1-132]
|
||||||
|
|
||||||
|
**Copy these conventions**
|
||||||
|
|
||||||
|
- Mock the store and API module, not network.
|
||||||
|
- Keep component tests structural and action-trigger oriented.
|
||||||
|
|
||||||
|
### `frontend/src/components/settings/__tests__/SettingsCloudTab.health.test.js`
|
||||||
|
|
||||||
|
**Closest analogs**
|
||||||
|
|
||||||
|
- Existing settings tab structure: [frontend/src/components/settings/__tests__/SettingsCloudTab.test.js:51-132]
|
||||||
|
- Store freshness/health state assertions: [frontend/src/stores/__tests__/cloudConnections.test.js:160-237]
|
||||||
|
|
||||||
|
**No exact analog**
|
||||||
|
|
||||||
|
- There is no dedicated health/test/reconnect settings suite yet.
|
||||||
|
|
||||||
|
## Shared Patterns to Reuse Everywhere
|
||||||
|
|
||||||
|
### Authentication / ownership
|
||||||
|
|
||||||
|
- Regular-user-only dependency: [backend/api/cloud/browse.py:29-31], [backend/api/cloud/connections.py:25-27], [backend/api/documents/content.py:24-26]
|
||||||
|
- Owner resolution before resource work: [backend/api/cloud/browse.py:205-214], [backend/api/cloud/connections.py:132-141], [backend/services/cloud_items.py:38-62]
|
||||||
|
|
||||||
|
### Router shape
|
||||||
|
|
||||||
|
- Thin routers with typed bodies + `account_limiter`: [backend/api/cloud/connections.py:281-304], [backend/api/cloud/browse.py:183-200], [backend/api/folders.py:122-149]
|
||||||
|
- Response shaping through explicit Pydantic schemas or dict helpers, never raw ORM/provider payloads: [backend/api/cloud/browse.py:64-97], [backend/api/cloud/schemas.py:17-67]
|
||||||
|
|
||||||
|
### Error translation
|
||||||
|
|
||||||
|
- `CloudConnectionError` becomes controlled reconnect guidance: [backend/api/documents/upload.py:160-175], [backend/api/documents/content.py:61-76]
|
||||||
|
- Incomplete/stale provider state becomes controlled warning, not fake success: [backend/services/cloud_items.py:319-342], [backend/api/cloud/browse.py:296-307]
|
||||||
|
|
||||||
|
### Audit
|
||||||
|
|
||||||
|
- Use `write_audit_log(...)` after successful state change, inside caller-owned transaction: [backend/services/audit.py:27-58]
|
||||||
|
|
||||||
|
### Provider boundary
|
||||||
|
|
||||||
|
- Providers normalize data and keep SDK/HTTP details private: [backend/storage/google_drive_backend.py:276-362], [backend/storage/onedrive_backend.py:300-386], [backend/storage/webdav_backend.py:241-332]
|
||||||
|
- Factories own provider construction and URL normalization: [backend/storage/cloud_backend_factory.py:7-66]
|
||||||
|
|
||||||
|
### Frontend architecture
|
||||||
|
|
||||||
|
- Thin view -> shared browser: [frontend/src/views/CloudFolderView.vue:1-20], [frontend/src/views/FileManagerView.vue:1-30]
|
||||||
|
- Shared browser owns interaction/layout/state: [frontend/src/components/storage/StorageBrowser.vue:424-681]
|
||||||
|
- Settings tab owns connection controls, not provider-specific subviews: [frontend/src/components/settings/SettingsCloudTab.vue:21-214]
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- Integration tests assert API contract plus DB side effect: [backend/tests/test_cloud.py:532-655], [backend/tests/test_cloud.py:1112-1249]
|
||||||
|
- Security tests assert absence of leaks and forbidden side effects: [backend/tests/test_cloud_security.py:197-301]
|
||||||
|
- View tests stub the browser for orchestration-only checks; rendered-flow tests use the real browser: [frontend/src/views/__tests__/CloudFolderView.test.js:51-101], [frontend/src/views/__tests__/CloudFolderRenderedFlow.test.js:170-205]
|
||||||
|
|
||||||
|
## No-Analog / planner watchlist
|
||||||
|
|
||||||
|
1. Provider-neutral mutation result dataclasses do not exist yet. Add them in `backend/storage/cloud_base.py`; do not leak provider payloads.
|
||||||
|
2. OAuth reconnect that patches an existing connection row does not exist yet. Current callback flow inserts a new row.
|
||||||
|
3. Shared paused upload queue UI for cloud conflicts/errors does not exist yet. Add it in `StorageBrowser.vue`, not a cloud-only component.
|
||||||
|
4. Authorized cloud-item preview/open endpoints do not exist yet. Reuse document content streaming patterns, but this is a new endpoint family.
|
||||||
|
5. Dedicated reconnect/audit/open-preview test suites do not exist yet; create them instead of overloading unrelated files.
|
||||||
|
|
||||||
|
## Recommended implementation order from pattern proximity
|
||||||
|
|
||||||
|
1. Extend `cloud_base.py` + provider backends + factory.
|
||||||
|
2. Add `cloud_operations.py` and keep all reconciliation through `cloud_items.py`.
|
||||||
|
3. Add `api/cloud/operations.py`, then minimally extend `connections.py` / `schemas.py`.
|
||||||
|
4. Extend `api/cloud.js` + `cloudConnections.js` + `CloudFolderView.vue`.
|
||||||
|
5. Extend `StorageBrowser.vue` and `SettingsCloudTab.vue`.
|
||||||
|
6. Add the missing dedicated backend/frontend test files before broad polish.
|
||||||
|
|
||||||
|
## PATTERN MAPPING COMPLETE
|
||||||
@@ -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 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<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 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: <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 / 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
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
---
|
||||||
|
phase: 13-virtual-local-cloud-operations
|
||||||
|
fixed_at: 2026-06-23T00:31:15Z
|
||||||
|
review_path: .planning/phases/13-virtual-local-cloud-operations/13-REVIEW.md
|
||||||
|
iteration: 1
|
||||||
|
findings_in_scope: 7
|
||||||
|
fixed: 7
|
||||||
|
skipped: 0
|
||||||
|
status: all_fixed
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 13: Code Review Fix Report
|
||||||
|
|
||||||
|
**Fixed at:** 2026-06-23T00:31:15Z
|
||||||
|
**Source review:** `.planning/phases/13-virtual-local-cloud-operations/13-REVIEW.md`
|
||||||
|
**Iteration:** 1
|
||||||
|
|
||||||
|
**Summary:**
|
||||||
|
- Findings in scope: 7 (CR-01 through CR-06, WR-06)
|
||||||
|
- Fixed: 7
|
||||||
|
- Skipped: 0
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fixed Issues
|
||||||
|
|
||||||
|
### CR-06: preview_cloud_file swallows HTTPException
|
||||||
|
|
||||||
|
**Files modified:** `backend/api/cloud/operations.py`
|
||||||
|
**Commit:** ebc74f4
|
||||||
|
**Applied fix:** Changed `except HTTPException: return _unsupported_preview_response(...)` to `except HTTPException: raise` so 401 (credential failure) and 404 (ownership race) are re-raised rather than masked as a 200 unsupported-preview response. The `except Exception` fallthrough for genuine provider errors is unchanged.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### CR-01: Content-Disposition header injection via unescaped filename
|
||||||
|
|
||||||
|
**Files modified:** `backend/api/cloud/operations.py`
|
||||||
|
**Commit:** d4b2697
|
||||||
|
**Applied fix:** Added `import urllib.parse` at the top of the module and replaced `filename.replace('"', "'")` + bare `filename=` header with `urllib.parse.quote(filename, safe=...)` producing an RFC 6266 `filename*=UTF-8''<percent-encoded>` header. This prevents injection of newlines, semicolons, and other HTTP header-special characters that the previous single-quote substitution did not cover.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### CR-02: Path traversal in WebDAV upload_file and rename
|
||||||
|
|
||||||
|
**Files modified:** `backend/storage/webdav_backend.py`
|
||||||
|
**Commit:** a1d1c3b
|
||||||
|
**Applied fix:**
|
||||||
|
- Added `PurePosixPath` to the existing `from pathlib import Path` import.
|
||||||
|
- In `upload_file`: applied `PurePosixPath(filename).name` before constructing `object_path`, with a fallback to `"upload"` for empty or dot-only results. Updated the returned `"name"` field to use the sanitized name.
|
||||||
|
- In `rename`: applied the same `PurePosixPath(new_name).name` guard before computing `new_path`, with a fallback to the original `new_name` (caller-validated). Updated the returned `"name"` field to use the sanitized name.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### CR-03: Audit log/DB failure after provider upload leaves orphaned upload
|
||||||
|
|
||||||
|
**Files modified:** `backend/api/cloud/operations.py`
|
||||||
|
**Commit:** 405c7a6
|
||||||
|
**Applied fix:** Wrapped the entire post-upload DB block (`upsert_cloud_item` + `update_folder_state` + `write_audit_log` + `session.commit()`) in a `try/except Exception` block. On failure, the session is rolled back and a `JSONResponse(207)` with `kind: "provider_success_db_error"` is returned, documenting that the file is on the provider but DocuVault has no metadata record. This makes the failure observable and actionable rather than an unhandled 500.
|
||||||
|
|
||||||
|
Note: this finding is classified as a logic/correctness concern — requires human verification that the 207 response is appropriate for the frontend conflict-action flow.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### CR-04: Successful cloud delete does not soft-delete the CloudItem row
|
||||||
|
|
||||||
|
**Files modified:** `backend/api/cloud/operations.py`
|
||||||
|
**Commit:** af0de30
|
||||||
|
**Applied fix:** After `kind == MUT_KIND_DELETED`, added a SQLAlchemy `sa_update(CloudItem).where(...).values(deleted_at=datetime.now(timezone.utc))` targeting `connection_id + provider_item_id + user_id + deleted_at.is_(None)`. This runs before `update_folder_state` and `write_audit_log` in the same transaction, so the soft-delete is committed atomically with the audit row and folder-state invalidation. Queries filtering on `deleted_at.is_(None)` (upload conflict check, preview, download) will no longer see the deleted file.
|
||||||
|
|
||||||
|
Note: this is a logic/correctness fix — requires human verification that the soft-delete target columns match the CloudItem model.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### CR-05: reconnect_connection audit log never committed
|
||||||
|
|
||||||
|
**Files modified:** `backend/api/cloud/connections.py`
|
||||||
|
**Commit:** 60df855
|
||||||
|
**Applied fix:** Added `await session.commit()` immediately after `write_audit_log(...)` in the reconnect endpoint, with an explanatory comment. The service's earlier `session.commit()` (persisting credentials) is a separate unit of work; this commit persists the audit row that was written to the session after that earlier commit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### WR-06: testConnection reads wrong field name (state vs status)
|
||||||
|
|
||||||
|
**Files modified:** `frontend/src/stores/cloudConnections.js`
|
||||||
|
**Commit:** b1a9f43
|
||||||
|
**Applied fix:** Changed `result?.state ?? 'unknown'` to `result?.status ?? 'unknown'` in `testConnection`. The server's health/test endpoints return `{ status: ... }` — the internal store vocabulary uses `state` but the translation must happen at the store boundary. Added a comment explaining the naming mismatch.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test Results
|
||||||
|
|
||||||
|
**Backend cloud tests** (test_cloud_mutations, test_cloud_reconnect, test_cloud_backends, test_cloud_audit):
|
||||||
|
183 passed, 3 xfailed, 4 warnings — 0 failures.
|
||||||
|
|
||||||
|
**Frontend store tests** (cloudConnections.test.js):
|
||||||
|
31 passed — 0 failures.
|
||||||
|
|
||||||
|
**Full backend suite** (excluding pre-existing `test_extract_docx` missing-module failure):
|
||||||
|
766 passed, 18 skipped, 4 deselected, 10 xfailed, 65 warnings — 0 failures.
|
||||||
|
|
||||||
|
The `test_extract_docx` failure is pre-existing (missing `python-docx` module in the local environment) and was failing on the main branch before any of these fixes were applied.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
_Fixed: 2026-06-23_
|
||||||
|
_Fixer: Claude (gsd-code-fixer)_
|
||||||
|
_Iteration: 1_
|
||||||
Reference in New Issue
Block a user