0ed514907f
- Add 04-02-SUMMARY.md with verification results and decisions - Update STATE.md to reflect plan 2/9 complete, next action 04-03
152 lines
12 KiB
Markdown
152 lines
12 KiB
Markdown
---
|
||
gsd_state_version: 1.0
|
||
milestone: v1.0
|
||
milestone_name: milestone
|
||
current_phase: 4
|
||
status: planned
|
||
last_updated: "2026-05-25T16:00:00.000Z"
|
||
progress:
|
||
total_phases: 5
|
||
completed_phases: 3
|
||
total_plans: 33
|
||
completed_plans: 15
|
||
percent: 60
|
||
---
|
||
|
||
# Project State
|
||
|
||
**Project:** DocuVault
|
||
**Status:** Phase 3 Complete — Ready to begin Phase 4
|
||
**Current Phase:** 4
|
||
**Last Updated:** 2026-05-25
|
||
|
||
## Phase Status
|
||
|
||
| Phase | Name | Status |
|
||
|---|---|---|
|
||
| 1 | Infrastructure Foundation | ✓ Complete |
|
||
| 2 | Users & Authentication | ✓ Complete (5/5 plans) |
|
||
| 3 | Document Migration & Multi-User Isolation | ✓ Complete (5/5 plans, 10/10 UAT, security gate passed) |
|
||
| 4 | Folders, Sharing, Quotas & Document UX | In Progress (1/9 plans complete) |
|
||
| 5 | Cloud Storage Backends | Not Started |
|
||
|
||
## Current Position
|
||
|
||
**Phase:** 04-folders-sharing-quotas-document-ux — In progress
|
||
**Plan:** 2/9 — Wave 0 scaffolds complete (04-01); migration 0004 + put_object_raw complete (04-02)
|
||
**Progress:** ██████░░░░ 60% (3/5 phases complete)
|
||
|
||
## Performance Metrics
|
||
|
||
| Metric | Value |
|
||
|---|---|
|
||
| Phases complete | 1 / 5 |
|
||
| Requirements mapped | 54 / 54 |
|
||
| Plans written | 5 (Phase 1) |
|
||
| Plans complete | 10 (5 Phase 1 + 5 Phase 2) |
|
||
|
||
## Accumulated Context
|
||
|
||
### Key Decisions
|
||
|
||
| Decision | Rationale |
|
||
|---|---|
|
||
| PostgreSQL + MinIO | Multi-user quotas and horizontal scaling require shared, consistent state |
|
||
| HKDF per-user key derivation | Single Fernet key would be catastrophic on leak — must be derived before first credential is stored |
|
||
| Presigned MinIO URL flow | FastAPI handles metadata only; bytes never pass through the API layer |
|
||
| Atomic PostgreSQL quota UPDATE | Never perform quota arithmetic in Python between two DB statements |
|
||
| JWT in httpOnly cookie | Refresh token in httpOnly cookie; access token in Pinia memory only — never localStorage |
|
||
| Refresh token family revocation | RFC 9700 — reuse of a rotated token revokes entire family and alerts user |
|
||
| BackgroundTasks replacement | FastAPI BackgroundTasks is per-instance; replace with Celery+Redis or pgqueuer before horizontal scale |
|
||
| AuditLog metadata_ ORM attribute | `metadata` is reserved on DeclarativeBase; ORM attribute is `metadata_` with `name="metadata"` kwarg to avoid silent collision |
|
||
| documents.user_id nullable Phase 1 | D-03 — no auth in Phase 1; Phase 2 migration adds NOT NULL after auth lands |
|
||
| groups stub table Phase 1 | D-02 — groups is a v2 feature; table created now for schema completeness, no rows until Phase 2+ |
|
||
| SEQUENCES grants in migration | GRANT USAGE/SELECT on sequences required for audit_log.id autoincrement nextval() by docuvault_app |
|
||
| Admin impersonation excluded | Explicit architectural exclusion — no endpoint or UI pathway; violates privacy-first core value |
|
||
| user_id as refresh token family proxy | No separate family_id column; user_id serves as family per RFC 9700 — simpler schema |
|
||
| pwdlib over passlib | pwdlib actively maintained with clean Argon2Hasher API; passlib unmaintained |
|
||
| TOTP replay TTL=90s | valid_window=1 covers ±30s (90s total) — TTL matches window |
|
||
| HIBP fail-open | Network errors return False + log warning; auth never blocked by external service |
|
||
| Two-DSN PostgreSQL strategy | DATABASE_URL (docuvault_app, DML only) + DATABASE_MIGRATE_URL (docuvault_migrate, DDL only); celery-worker gets only DATABASE_URL |
|
||
| MinIO healthcheck via mc ready local | curl removed from MinIO Docker image since Oct 2023; mc is the correct in-container healthcheck tool |
|
||
| pydantic-settings v2 SettingsConfigDict | SettingsConfigDict API used (not deprecated class Config form) for env var config |
|
||
| async_client fixture name | Distinct from legacy sync `client` fixture to avoid collision; both coexist until Plan 05 |
|
||
| xfail(strict=False) for Wave 0 | All pre-implementation scaffolds use strict=False so unexpected passes don't break CI |
|
||
| StorageBackend ABC + factory mirrors ai/ pattern | 5 abstract methods; get_storage_backend() factory; MinIOBackend wraps all sync Minio SDK calls in asyncio.to_thread() |
|
||
| STORE-02 key enforced in code | MinIOBackend.put_object constructs {user_id}/{document_id}/{uuid4()}{ext}; no filename parameter — only extension passes through |
|
||
| null-user D-03 sentinel | services/storage.save_upload uses user_id="null-user" in Phase 1 (no auth); Phase 2 replaces with str(current_user.id) |
|
||
| load_settings flat-file Phase 1 | users.ai_provider/ai_model columns cannot be populated until Phase 2; settings remain flat-file JSON for Phase 1 |
|
||
| Deferred Celery import in /password-reset | send_reset_email.delay called via from tasks.email_tasks import send_reset_email inside handler body — same circular-import fix as document_tasks |
|
||
| TOTP QR code as otpauth:// link | No QR library installed; plan permits manual secret display for MVP; functional flow complete without rendered QR image |
|
||
| ConfirmBlock no acknowledgment checkbox | ConfirmBlock handles message + button pair; BackupCodesDisplay owns its separate acknowledgment checkbox — no overlap |
|
||
| ADMIN-07 enforced by omission | No impersonation endpoint exists; AST check + test_admin_impersonation_not_found verify absence; violates privacy-first core value |
|
||
| _user_to_dict() whitelist for admin responses | Explicit field whitelist prevents accidental password_hash/credentials_enc leakage from admin endpoints |
|
||
| Quota warning is 200 not 4xx | Below-usage limit change is applied; warning=True advisory field returned — not a rejection |
|
||
| AdminQuotasTab fetches quotas per-user via Promise.allSettled | adminListUsers() does not include quota fields; per-user endpoint parallelized; failed quotas filtered silently |
|
||
| Temp password via crypto.getRandomValues | Browser-native CSPRNG; no external library; always satisfies AUTH-01 strength rules |
|
||
| batch_alter_table for NOT NULL in migration 0003 | SQLite requires batch_alter_table for ALTER COLUMN; transparent passthrough on PostgreSQL — enables SQLite CI test runs |
|
||
| MinIO step in migration 0003 gated on MINIO_ENDPOINT | Migration skips MinIO deletions when env var absent; enables safe SQLite test runs per T-03-02 |
|
||
| raising=False for Phase 3 MinIO mock fixtures | mock_minio_presigned + mock_minio_stat patch methods that don't exist until Plan 03-02; raising=False pre-installs them |
|
||
| Dual MinIO client (internal + public) | Presigned URL HMAC signature must be computed with browser-visible hostname (localhost:9000); using internal Docker client (minio:9000) causes browser signature mismatch |
|
||
| Wave 2 user_id=None guard | upload-url sets user_id=None + object_key "null-user/" prefix; confirm skips quota when user_id is None; Plan 03-03 removes both guards |
|
||
| SQLite quota xfail(strict=False) | SQLite stores UUID as CHAR(32) without dashes; raw SQL WHERE user_id = :uid never matches str(uuid) dashed format — test-env limitation, not code defect |
|
||
| Celery mock required in /confirm tests | extract_and_classify.delay() connects to Redis; monkeypatch blocks it in unit tests; MagicMock pattern established for all confirm endpoint tests |
|
||
| get_regular_user raises 403 for admin | Admin is authenticated but must not access document content; 401 would falsely imply unauthenticated — 403 is correct for role rejection |
|
||
| Cross-user doc access returns 404 not 403 | Combining "not found" and "wrong owner" into 404 prevents attacker from learning which doc IDs exist for other users (D-16, T-03-11) |
|
||
| CASE WHEN replaces GREATEST in quota decrement | SQLite lacks GREATEST scalar function; CASE WHEN used_bytes > :delta THEN used_bytes - :delta ELSE 0 END is semantically equivalent and SQLite-compatible |
|
||
| load_topics_for_user uses or_(user_id == x, user_id.is_(None)) | SQLAlchemy is_(None) not == None; or_() combines system topics and user's own topics for namespace-scoped query (D-17, DOC-04) |
|
||
| AI-suggested topics go in user namespace | classifier passes user_id=doc.user_id to create_topic; AI-suggested topics are per-user not system-wide (D-11) |
|
||
| Celery task signature unchanged for ai_provider | Task receives only document_id; ai_provider/ai_model resolved inside _run via session.get(User, doc.user_id) — prevents broker injection (T-03-19) |
|
||
| _DEFAULT_SYSTEM_PROMPT in classifier.py | System prompt env var is optional; hardcoded fallback kept in classifier module not config.py (D-13) |
|
||
| Default AI provider is ollama/llama3.2 | Code defaults; overridable via DEFAULT_AI_PROVIDER / DEFAULT_AI_MODEL env vars (D-15) |
|
||
| /settings route kept as static placeholder | SettingsView shows admin-managed card; route not removed to avoid UX regression (Risk 6) |
|
||
| Plain anchor in quota rejection block | <a href="/settings"> used instead of <router-link> to avoid import dependency in upload component |
|
||
| uploadProgress entries owned by parent | Store does not clear uploadProgress map entries after upload; DropZone/parent clears on row dismiss |
|
||
| fetchQuota silent catch in auth store | Silent catch keeps last-known values; QuotaBar owns loadFailed state and hides on error (UI-SPEC) |
|
||
| XHR PUT progress range 5–90 | 5 + Math.round(pct * 0.85) maps XHR 0-100 → visual 5-90; remaining 10% covers confirm + enqueue |
|
||
| FTS stubs carry both xfail and skipif(INTEGRATION) | skipif fires first in non-INTEGRATION runs (tests appear SKIPPED); xfail catches failures when INTEGRATION=1 — both decorators required |
|
||
| Wave 0 stubs: single-line body only | All Phase 4 stubs: body is only pytest.xfail("not implemented yet") — no assertion code; strict=False so xpass never breaks CI |
|
||
| GIN index via op.execute() raw SQL | Alembic autogenerate cannot round-trip expression indexes; raw SQL with comment prevents re-creation on every --autogenerate run (issue #1390) |
|
||
| put_object_raw not in StorageBackend ABC | audit-logs bucket is MinIO-only; local/WebDAV backends have no audit concept; MinIOBackend-only method |
|
||
|
||
### Open Questions
|
||
|
||
- Verify cloud SDK minor versions on PyPI before Phase 5 pinning
|
||
|
||
### Workflow Changes (2026-05-25)
|
||
|
||
Two mandatory cross-cutting gates added to all phases going forward:
|
||
|
||
**1. Test gate** — every plan must leave `pytest -v` passing with zero failures. Every new function/endpoint/component requires at least one test. All security-invariant negative tests (wrong owner, admin block, token replay) must exist and pass.
|
||
|
||
**2. Security gate** — a security agent runs after every plan execution and is a blocking requirement before phase advancement. It:
|
||
|
||
- Runs `bandit -r backend/`, `pip audit`, `npm audit --audit-level=high`
|
||
- Checks for path traversal, IDOR, SSRF, timing attacks, mass assignment, token replay
|
||
- Verifies admin endpoints never return `password_hash`, `credentials_enc`, or document content
|
||
- Fixes issues directly (full edit access) rather than deferring
|
||
|
||
**3. Bug fix rule** — all fixes: root cause only, ≤50 lines, regression test required, no workarounds.
|
||
|
||
See CLAUDE.md "Testing Protocol" and "Security Protocol" sections for full detail.
|
||
|
||
### Blockers
|
||
|
||
None.
|
||
|
||
## Session Continuity
|
||
|
||
_Updated at each phase transition._
|
||
|
||
| Field | Value |
|
||
|---|---|
|
||
| Last session | 2026-05-25 — Phase 3 UAT complete (10/10); security gate passed (3 fixes: bandit B324, Referrer-Policy, IDOR on /topics/suggest); test fix for test_lmstudio.py import |
|
||
| Last session | 2026-05-25 — Phase 4 context gathered (4 areas: folder nav, sharing, PDF proxy, audit log) |
|
||
| Last session | 2026-05-25 — Phase 4 UI-SPEC approved (6 dimensions: 2 PASS clean, 3 FLAG non-blocking, 0 BLOCK) |
|
||
| Last session | 2026-05-25 — Phase 4 plans created (9 plans, 7 waves) + verification passed (0 blockers, 2 warnings) |
|
||
| Last session | 2026-05-25 — Plan 04-01 executed: 30 Wave 0 xfail stubs across 5 test files; 39 xfailed total, zero new failures |
|
||
| Last session | 2026-05-25 — Plan 04-02 executed: migration 0004 (pdf_open_mode, GIN FTS index, audit-logs bucket) + MinIOBackend.put_object_raw(); 122 tests pass |
|
||
| Next action | Continue Wave 1 execution: run plan 04-03 |
|
||
| Pending decisions | None |
|
||
| Resume file | `.planning/phases/04-folders-sharing-quotas-document-ux/04-02-SUMMARY.md` |
|