Files
kite/.planning/phases/14.1-cloud-local-file-parity-hardening/14.1-02-PLAN.md
T

16 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
14.1-cloud-local-file-parity-hardening 02 execute 2
14.1-01
backend/api/cloud/schemas.py
backend/services/cloud_items.py
backend/api/cloud/operations.py
backend/services/cloud_analysis.py
backend/api/cloud/analysis.py
true
CLOUD-02
ANALYZE-01
ANALYZE-05
ANALYZE-06
ANALYZE-07
CACHE-03
CACHE-05
truths artifacts key_links
An owner can fetch a cloud item detail with extracted text, analysis status, topics, and source metadata through an authorized endpoint
The cloud detail response never includes credentials_enc, object_key, version_key, or raw provider URLs
A foreign user gets 404 and an admin is blocked from cloud detail
Default analysis enqueue still skips already-current items; force=true re-queues them without mutating the provider
Failed-item retry with no surviving active job creates a single-item retry job through the authorized analysis path
Cloud detail resolution downloads zero provider bytes
path provides contains
backend/api/cloud/schemas.py CloudItemDetailOut schema (allowlist, analysis fields, no object_key/credentials_enc) and force field on AnalysisEnqueueRequest class CloudItemDetailOut
path provides contains
backend/services/cloud_items.py resolve_owned_cloud_item_detail — owner-scoped detail + topic resolution, raises ValueError/domain exception resolve_owned_cloud_item_detail
path provides contains
backend/api/cloud/operations.py GET cloud item detail route returning CloudItemDetailOut, metadata-only (no byte hydration) /detail
path provides contains
backend/services/cloud_analysis.py force re-analyze bypass of already_current and a single-item retry-job creation helper force
path provides contains
backend/api/cloud/analysis.py enqueue route passes force through; retry route falls back to single-item job creation force
from to via pattern
backend/api/cloud/operations.py backend/services/cloud_items.py detail route calls resolve_owned_cloud_item_detail resolve_owned_cloud_item_detail
from to via pattern
backend/api/cloud/analysis.py backend/services/cloud_analysis.py enqueue route forwards force= to enqueue_analysis_job force=
Add the backend surface that makes cloud files behave like local documents: an owner-scoped cloud item detail endpoint that returns extracted text, analysis status, topics, and subtle source metadata through a strict credential-free schema; a force re-analyze flag that lets a user re-queue an already-current cloud item; and a single-item retry-job creation path so a failed item can be retried even when no active job survives.

Purpose: Implements D-01..D-08, D-11, D-12, D-15, D-16 backend contracts so the frontend (Plan 03/04) can render parity without forking. Satisfies CLOUD-02 (authorized open/preview/view), ANALYZE-05 (retry), ANALYZE-06 (idempotency with explicit force override), ANALYZE-07 (no provider mutation), CACHE-03/CACHE-05 (bytes only via cache, owner-scoped). Output: CloudItemDetailOut schema, resolve_owned_cloud_item_detail service helper, GET detail route, force-aware enqueue, single-item retry-job path — all covered by Plan 01 tests turning GREEN.

<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/14.1-cloud-local-file-parity-hardening/14.1-CONTEXT.md @.planning/phases/14.1-cloud-local-file-parity-hardening/14.1-RESEARCH.md @CLAUDE.md @backend/api/cloud/schemas.py @backend/services/cloud_items.py @backend/api/cloud/operations.py @backend/services/cloud_analysis.py @backend/api/cloud/analysis.py @backend/db/models.py @backend/tests/test_cloud_detail_parity.py @backend/tests/test_cloud_reanalyze_force.py Task 1: CloudItemDetailOut schema + resolve_owned_cloud_item_detail service + GET detail route - backend/api/cloud/schemas.py (CloudItemOut, CloudCapabilityOut, CacheStatusOut allowlist style — model the new schema on these) - backend/services/cloud_items.py (resolve_owned_connection, list_cloud_children, ListingResult — reuse owner-scoping pattern; service raises ValueError/domain exception, never HTTPException) - backend/api/cloud/operations.py (open/preview/download routes, _resolve_and_get_adapter, get_regular_user dependency, parse_uuid usage, JSONResponse vs HTTPException convention) - backend/db/models.py (CloudItem fields: extracted_text, analysis_status, semantic_index_status, provider_size, content_type, modified_at, parent_ref, path_snapshot; CloudItemTopic ↔ Topic join; CloudConnection.provider/display_name) - backend/tests/test_cloud_detail_parity.py (the exact field names and forbidden tokens the route must satisfy) - .planning/phases/14.1-cloud-local-file-parity-hardening/14.1-CONTEXT.md (D-04, D-05, D-07, D-08, D-15, D-16, D-18) In backend/api/cloud/schemas.py add class CloudItemDetailOut(BaseModel) as an explicit allowlist modeled on CloudItemOut + CacheStatusOut. Fields (and ONLY these — no credentials_enc, object_key, version_key, provider URL, token, fingerprint): id (DocuVault UUID str), provider_item_id, name, kind, parent_ref, content_type, size, modified_at, etag, provider (connection provider string), display_name (connection display name), location (human path_snapshot or parent_ref, no provider URL), analysis_status, semantic_index_status, extracted_text (Optional[str]), topics (list[str] of topic names), capabilities (dict[str, CloudCapabilityOut]), unsupported_analysis_reason (Optional[str], populated from capabilities when analyze is unsupported per D-16), is_stale (bool derived from analysis_status == "stale" per D-07). Document at class top that object_key/credentials_enc are absent by design (mirror the T-14-02 docstring pattern). In backend/services/cloud_items.py add async def resolve_owned_cloud_item_detail(session, *, user_id, connection_id, provider_item_id) that: resolves the connection via resolve_owned_connection (owner check), selects the CloudItem by (connection_id, provider_item_id) scoped to user_id, raises a domain exception / ValueError when not found (router maps to 404), loads topic names via a CloudItemTopic→Topic join, and returns a plain dataclass/dict the router maps into CloudItemDetailOut. This helper performs metadata-only DB reads — it MUST NOT call adapter.get_object or hydrate_and_cache_bytes (T-14-04 / CACHE-03). In backend/api/cloud/operations.py register GET /connections/{connection_id}/items/{item_id:path}/detail with response_model=CloudItemDetailOut and dependency get_regular_user (admin blocked); parse connection_id with parse_uuid; call resolve_owned_cloud_item_detail; on the domain not-found exception raise HTTPException(404) (router layer translates, per CLAUDE.md service-vs-router rule); never decrypt or expose credentials. Do not add a parallel detail router file — keep it on the existing operations router so it shares the /api/cloud prefix. cd backend && python -m pytest tests/test_cloud_detail_parity.py -x 2>&1 | tail -20 - `grep -c 'class CloudItemDetailOut' backend/api/cloud/schemas.py` returns 1. - CloudItemDetailOut has no forbidden fields: `grep -E 'object_key|credentials_enc|version_key' backend/api/cloud/schemas.py | grep -A0 -i detail` is empty within the class body (manual confirm the class block excludes them). - `grep -c 'resolve_owned_cloud_item_detail' backend/services/cloud_items.py` returns >= 1 (definition present). - The detail route exists: `grep -F '/detail' backend/api/cloud/operations.py` returns a route line with `response_model=CloudItemDetailOut`. - The detail service does not hydrate bytes: `grep -n 'get_object\|hydrate_and_cache_bytes' backend/services/cloud_items.py` shows no new call inside resolve_owned_cloud_item_detail. - test_cloud_detail_parity.py passes: owner gets fields, foreign user 404, admin 403, response has no forbidden tokens, stale preserves extracted_text/topics. Cloud detail endpoint returns analysis fields + source metadata for the owner through a credential-free schema, blocks foreign user/admin, and downloads no bytes; Plan 01 detail tests pass. Task 2: Force re-analyze flag + single-item retry-job creation - backend/services/cloud_analysis.py (enqueue_analysis_job signature, already_current branch ~line 546-660, retry_job_item ~line 1049, _check_already_current, EnqueueResult) - backend/api/cloud/schemas.py (AnalysisEnqueueRequest — add force; AnalysisEnqueueOut) - backend/api/cloud/analysis.py (enqueue_job route ~line 137, retry route, _build_job_out, AnalysisControlOut) - backend/tests/test_cloud_reanalyze_force.py (exact force=true and single-item retry assertions) - .planning/phases/14.1-cloud-local-file-parity-hardening/14.1-CONTEXT.md (D-11, D-12) - .planning/phases/14.1-cloud-local-file-parity-hardening/14.1-RESEARCH.md (Forced Re-Analyze Pattern section) Add force: bool = Field(default=False) to AnalysisEnqueueRequest in backend/api/cloud/schemas.py (default preserves existing idempotency). Extend enqueue_analysis_job in backend/services/cloud_analysis.py to accept force: bool = False; when force is True, skip the already_current branch so supported items are created as queued job items even if their version_key matches an indexed item — the already_current check becomes (already_current and not force). Forced enqueue must still: validate owner/connection/item and unsupported type (unsupported items stay unsupported), compute version_key/fingerprint normally, and create queued items only — it MUST NOT call any provider mutation (no rename/move/delete) and MUST NOT bypass the cache lifecycle; processing remains process_cloud_analysis_item + hydrate_and_cache_bytes (ANALYZE-07, CACHE-03). Wire force through the enqueue_job route in backend/api/cloud/analysis.py: pass body.force into enqueue_analysis_job(...). For D-12, add an owner-scoped single-item retry path: when a failed item cannot be retried within an existing job (no surviving active job, or retry_job_item raises AnalysisJobNotFound for that cloud_item_id), create a one-item analysis job for that cloud_item_id via the existing enqueue_analysis_job(scope="file", provider_item_ids=[provider_item_id], force=True) so a fresh queued item is produced through the authorized path. Implement this fallback either in the retry route handler (catch the not-found/invalid-state domain exception and create the single-item job) or as a thin service helper (e.g. retry_or_create_single_item_job) in cloud_analysis.py — choose the service helper if both the route and tests need it. Keep all aggregate counters consistent and return the existing typed result schemas (AnalysisControlOut / AnalysisEnqueueOut). Do not raise HTTPException from the service layer — raise domain exceptions and translate in the route. cd backend && python -m pytest tests/test_cloud_reanalyze_force.py tests/test_cloud_analysis_contract.py -x 2>&1 | tail -20 - `grep -c 'force' backend/api/cloud/schemas.py` shows force added to AnalysisEnqueueRequest (>= 1 occurrence near the class). - enqueue_analysis_job accepts force: `grep -n 'def enqueue_analysis_job' backend/services/cloud_analysis.py` and the signature/body reference force. - The already_current branch respects force: `grep -n 'already_current and not force\|not force' backend/services/cloud_analysis.py` returns >= 1. - The enqueue route forwards force: `grep -F 'force=' backend/api/cloud/analysis.py` returns >= 1 (or body.force passed positionally is visible). - Single-item retry fallback exists: `grep -nE 'retry_or_create_single_item_job|scope="file"|single' backend/services/cloud_analysis.py backend/api/cloud/analysis.py` shows the fallback path. - test_cloud_reanalyze_force.py passes: default skips already-current, force re-queues, no provider mutation, single-item retry yields one queued item. test_cloud_analysis_contract.py still passes (no regression in default idempotency). force=true re-queues already-current items without provider mutation, default enqueue is unchanged, and failed items can be retried via a single-item job when no active job exists; Plan 01 force/retry tests pass with no idempotency regression.

<threat_model>

Trust Boundaries

Boundary Description
browser → GET detail connection_id/provider_item_id untrusted; must resolve under current_user.id only
browser → POST enqueue (force) force is a user-driven flag; must not enable cross-user enqueue or provider mutation
service → DB metadata-only reads; no byte hydration during detail

STRIDE Threat Register

Threat ID Category Component Disposition Mitigation Plan
T-14.1-03 Information Disclosure CloudItemDetailOut mitigate Allowlist schema excludes credentials_enc/object_key/version_key/provider URL; Plan 01 no-leak test enforces
T-14.1-04 Elevation of Privilege detail route + force enqueue mitigate get_regular_user (admin 403) + resolve under current_user.id (foreign 404); tests enforce
T-14.1-05 Tampering forced re-analyze mitigate Force only re-queues analysis; processing path unchanged (no rename/move/delete adapter call); ANALYZE-07 test enforces
T-14.1-06 Information Disclosure detail byte access mitigate Detail resolution is metadata-only; never calls get_object/hydrate_and_cache_bytes (CACHE-03)
T-14.1-SC Tampering npm/pip installs accept No package installs (RESEARCH Package Legitimacy Audit: none)
</threat_model>
- `cd backend && python -m pytest tests/test_cloud_detail_parity.py tests/test_cloud_reanalyze_force.py -x` passes. - `cd backend && python -m pytest tests/test_cloud_analysis_contract.py tests/test_cloud_security.py -x` passes (no regression). - `cd backend && python -m pytest -q 2>&1 | tail -5` — full backend suite passes.

<success_criteria>

  • CloudItemDetailOut exists as a credential-free allowlist with analysis fields + source metadata.
  • resolve_owned_cloud_item_detail resolves owner-scoped detail + topics with zero byte hydration.
  • GET detail route is owner-scoped (foreign 404, admin 403).
  • force=true re-queues already-current items; default enqueue unchanged; no provider mutation.
  • Single-item retry job creation works when no active job survives. </success_criteria>

<artifacts_produced>

Artifacts this phase produces (Plan 02)

  • Class: CloudItemDetailOut (backend/api/cloud/schemas.py) — credential-free cloud detail response with extracted_text, analysis_status, semantic_index_status, topics, provider, display_name, location, capabilities, unsupported_analysis_reason, is_stale.
  • Field: force: bool on AnalysisEnqueueRequest (backend/api/cloud/schemas.py).
  • Function: resolve_owned_cloud_item_detail(session, *, user_id, connection_id, provider_item_id) (backend/services/cloud_items.py).
  • Route: GET /api/cloud/connections/{connection_id}/items/{item_id:path}/detail → CloudItemDetailOut (backend/api/cloud/operations.py).
  • Parameter: force: bool = False on enqueue_analysis_job (backend/services/cloud_analysis.py).
  • Function/path: single-item retry-job creation fallback (e.g. retry_or_create_single_item_job) in backend/services/cloud_analysis.py and/or the retry route in backend/api/cloud/analysis.py. </artifacts_produced>
Create `.planning/phases/14.1-cloud-local-file-parity-hardening/14.1-02-SUMMARY.md` when done