After the ES256 upgrade, create_access_token requires non-empty
jwt_private_key / jwt_public_key. The auth_user, second_auth_user,
and admin_user fixtures in conftest.py call create_access_token
without keys being set, breaking all tests that exercise the auth
system. Add a function-scoped autouse fixture that provisions a
generated P-256 keypair for every test.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- docker-compose.yml: add JWT_PRIVATE_KEY + JWT_PUBLIC_KEY to backend and celery-worker service env blocks
- README.md: add JWT_PRIVATE_KEY/JWT_PUBLIC_KEY to required env vars table; add JWT Key Generation section with Python one-liner and warning about session revocation
- .env.example: add JWT_PRIVATE_KEY= and JWT_PUBLIC_KEY= adjacent to SECRET_KEY
- backend/main.py: version bump 0.1.1 -> 0.1.2
- frontend/package.json: version bump 0.1.1 -> 0.1.2
- Documents 9 xfail stubs created + test_settings_has_jwt_config extension
- Records test counts: 372 passed / 1 failed / 9 xfailed
- Includes promotion plan mapping each stub to its Plan 02/03 task
- Confirms no production code was modified in this Wave 0 plan
Audited post-execution state: all 11 tasks (9 original + CR-02 gap fix +
activate anti-pattern guard) have green automated tests. 84/84 passing.
Zero xfail stubs remaining. Sign-off approved.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All Phase 7.2 behaviors verified end-to-end against the running app:
- JTI UUID claim present in every issued access token
- Token rejected (Session invalidated) after password change
- Refresh cookie survives password change (skip_token_hash)
- Token rejected after enable-TOTP and disable-TOTP
- Token rejected after admin deactivation
- Reactivation does NOT write user_nbf (no spurious revocation)
Findings recorded in UAT.md:
- Workers must be restarted after Phase 7.2 deploy (--workers 2 does not hot-reload)
- D-02 same-second edge case confirmed benign in real usage
- Deactivation fires is_active check before user_nbf in same-second scenarios (belt-and-suspenders)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Added "jti": str(uuid.uuid4()) to the access token payload in services/auth.py
- Promoted Wave 0 xfail stub test_create_access_token_includes_jti_claim to PASS
- Added test_create_access_token_jti_is_unique_per_call uniqueness guard
- uuid module was already imported at module top — no new imports needed
- Add test_change_password_writes_user_nbf_to_redis (xfail strict=False)
- Add test_enable_totp_writes_user_nbf_to_redis (xfail strict=False)
- Add test_disable_totp_writes_user_nbf_to_redis (xfail strict=False)
- Each test exercises the full auth flow then asserts user_nbf Redis key
- Wave 2 (Plan 03) promotes stubs by replacing pytest.xfail() with real assertion
- All 27 existing auth API tests unaffected
- Add test_create_access_token_includes_jti_claim with xfail(strict=False)
- Asserts 'jti' key present in decoded payload, parseable as UUID
- Wave 1 (Plan 02) will add jti to create_access_token to promote this stub
- All 17 existing tests unaffected
Results from Phase 6 production hardening load run:
stats, stats_history, failures, and exceptions captured for baseline
performance reference.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Audits all 54 requirements and 9 phases against implementation.
Scores: 53/54 requirements met, 53/54 integration checks passed,
5/6 user flows complete. AUTH-07 partial gap (CR-01..03) documented
and resolved by Phase 7.1. Tech-debt items catalogued for next milestone.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
06-PATTERNS.md: maps 12 new/modified files to closest codebase analogs
for Phase 6 (Performance & Production Hardening).
06-REVIEW-FIX.md: records all 16 review findings (7 critical + 9 warning)
fixed in iteration 1 on 2026-06-04.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All 7 phases + sub-phases 6.1, 6.2, 7.1 complete as of 2026-06-05.
HANDOFF.json removed — no active handoff; STATE.md updated to reflect
v1.0 milestone done and next-action as "ready for next milestone."
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All 11 test functions confirmed present and implemented. 10 pass in unit
mode; test_load_provider_config is correctly gated behind INTEGRATION=1
(requires PostgreSQL) — not a gap, test is fully implemented.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two fixes from manual SLA validation (06-LOCUST-01):
1. backend/api/auth.py: concurrent registrations with same email/handle caused
UniqueViolation to bubble as 500. Now wraps session.commit() in try/except
IntegrityError → 409 Conflict. Regression covered by existing auth API tests.
2. backend/load_tests/locustfile.py: rewrote from single-shared-account to
pre-authenticated unique accounts. New @events.test_start listener creates
50 accounts in 9-user batches (65 s pause between batches to stay under
10 req/min IP rate limit). Added min-request guard to SLA check so a
run-time-consumed-by-setup result never silently "passes".
Run command updated to --run-time 12m.
Known open issue: _account_key in services/rate_limiting.py falls back to IP
when request.state.current_user is not yet set (rate limiter key_func runs
before handler body). Fix: extract sub from JWT directly. See next session.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Audited Phase 6 VALIDATION.md: confirmed all 10 automated tasks were already
implemented and passing. Updated statuses from pending→green, wave_0_complete
and nyquist_compliant set to true. 4 manual items (Docker/Locust/Scout) remain
pending as expected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DocumentCard classification_failed badge requires doc.status from the
API list endpoint. _doc_to_dict was omitting the field; this fix adds
it and adds a regression test that would have caught the omission.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- GET /api/admin/ai-config/models — fetch model list from provider's /models endpoint
- POST /api/admin/ai-config/test-connection — replaced GET; accepts unsaved form
values (api_key, base_url, model_name) so admins can test before saving
- TestConnectionRequest Pydantic model with provider_id validation
- SearchableModelSelect.vue — new reusable combobox; Teleport to body avoids
overflow:hidden clipping; shows all models on open, filters only on typing;
static "Enter manually" item always pinned at bottom; caches per provider
- AdminAiConfigTab: model name input replaced with SearchableModelSelect;
testingProvider ref + spinner added to Test Connection button
- DocumentCard: processing spinner + "Classifying…", pending "Queued" badge,
classification_failed badge; topics section only shown when status=ready
- FileManagerView: onUnmounted imported (polling wiring pending next session)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three root causes fixed:
1. docker-compose.yml — celery-worker and celery-beat missing PYTHONPATH=/app
ForkPoolWorker processes inherit sys.path with '' (CWD), but fork can change
CWD so lazy imports like `from db.session import ...` raised ModuleNotFoundError.
Adding PYTHONPATH=/app ensures /app is always explicit in the path.
2. provider_config.py — lmstudio SUPPORTS_JSON_MODE set to False
LM Studio only accepts response_format type 'json_schema' or 'text', not
'json_object'. GenericOpenAIProvider now omits response_format for lmstudio
and relies on parse_classification() fallback (same path as Gemini).
3. generic_openai_provider.py — max_tokens raised 1024→4096 (classify) / 256→1024 (suggest)
Qwen3 thinking models (like qwen/qwen3.5-9b) consume reasoning tokens from the
same budget. With max_tokens=1024 the model exhausts the budget in the thinking
trace, leaving content=''. 4096 gives room for both thinking and the JSON output.
Also updates lmstudio PROVIDER_DEFAULTS model to qwen/qwen3.5-9b (confirmed loaded
in LM Studio) and context_chars to 32_000 (Qwen3 context window).
Tested: classify(invoice_text, ['Finance','Legal','HR']) → topics=['Finance'] ✓
Backend tests: 376 passed ✓
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The test_audit_log_filter_by_event_type test was passing the full event
type string ("document.uploaded") as the event_type filter parameter.
Update to pass the validated prefix ("document") matching the new
allowlist semantics, and assert item event_type starts with "document."
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add fetchError ref initialized to null. Clear it at the start of
fetchLog() and set it on catch. Add <p v-else-if="fetchError"> block
in template between loading and empty states so admin users see
"Failed to load audit log. Please try again." instead of the
empty-state message when an API error occurs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace print(..., file=sys.stderr) with structlog.get_logger(__name__)
.warning("cloud_delete_failed", ...) so cloud delete errors carry a
correlation ID, are picked up by promtail, and appear in Loki — matching
the Phase 6 goal of routing all logging through structlog.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add the 10.0.0.0/8 RFC-1918 block to _TRUSTED_PROXY_NETS so reverse
proxies in cloud VPCs and Kubernetes clusters are recognized. Without
this, X-Forwarded-For was silently ignored for any proxy on a 10.x.x.x
address, logging all requests as originating from the proxy IP.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The GET /api/documents/ response is {items: [...], total: N, ...} not a
bare list. Change docs = resp.json() to resp.json().get("items", []) so
get_document task correctly accesses docs[0]["id"] without TypeError.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add auth_limiter (IP-level limiter from api.auth) to the reset_rate_limiter
autouse fixture in conftest.py, alongside the existing account_limiter reset.
Prevents spurious 429s when tests call /api/auth/* endpoints in a tight loop.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add _VALID_EVENT_PREFIXES frozenset and validate event_type at all three
filter sites in audit.py (_build_filtered_query, _build_filtered_query
_with_handles, and the inline count query in list_audit_log). Invalid
values return HTTP 422. Also change like() pattern from prefix% to
prefix.% to enforce true prefix-match semantics.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Capture response status code in send_with_header closure via nonlocal.
After awaiting the app, emit a structured log line via
structlog.get_logger("docuvault.access").info("request_complete", ...)
so duration_ms is actually written to the log before contextvars are
cleared by the next request.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace raw email in auth.login_failed metadata_ with a 16-char
SHA-256 prefix (attempted_email_hash). Update AuditLogTab.vue to
display the hash field instead. This removes plaintext email PII from
the unencrypted audit_logs JSONB column as required by CLAUDE.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add _VALID_BACKENDS frozenset and validate body.backend before writing
to DB in PATCH /api/users/me/default-storage. Returns HTTP 422 for
any value not in the allowlist, preventing mass-assignment and logic
bypass via arbitrary string values.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace all instances of request.headers.get("X-Forwarded-For") in
cloud.py (connect_webdav, delete_connection) and documents.py (upload,
confirm, delete) with get_client_ip(request) from deps.utils. Add
get_client_ip import to both files. Prevents XFF spoofing in audit logs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CR-01: add CLOUD_CREDS_KEY to backend service environment
- CR-02: disable Grafana anonymous access, add admin credentials, bind Grafana and Loki to loopback (127.0.0.1)
- WR-02: replace --reload with --workers 2 for production backend
- WR-05: add read_only/tmpfs/cap_drop/no-new-privileges to celery-beat; redirect schedule file to /tmp
- WR-06: change LOG_JSON from hardcoded true to ${LOG_JSON:-true} for env-var override
- WR-08: add SECRET_KEY to celery-worker environment
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace synchronous await classifier.classify_document() with extract_and_classify.delay()
- Set doc.status='processing' and commit before dispatching Celery task
- Return {'document_id': str, 'status': 'processing'} instead of assigned topics
- Promote test_reclassify_requeues_celery from xfail stub to passing test
- Add test_reclassify_cross_user_returns_404 for IDOR security invariant (T-07-10)
- Add _ClassificationError sentinel raised by _run() for classification failures
- Add _mark_classification_failed() async helper for final status writeback
- Change decorator to bind=True, max_retries=3
- Outer sync task catches _ClassificationError and calls self.retry(countdown=[30,90,270])
- MaxRetriesExceededError caught in nested try/except to avoid re-raise from exc block
- Promote test_retry_backoff and test_exhaustion_sets_failed_status from xfail to passing
- Tests use push_request(retries=N) + patch.object(task, "retry") to verify countdown values
- test_exhaustion_sets_failed_status uses assert_called_once_with (asyncio.run calling convention)
- Remove _ProviderConfigStub from services/ai_config.py (replaced by real ProviderConfig)
- Add module-level import: from ai.provider_config import ProviderConfig, PROVIDER_DEFAULTS
- load_provider_config() now returns ProviderConfig (no lazy import inside function body)
- classifier.py: replace inline _settings dict with load_provider_config(session) call (D-06)
- Per-user override path builds ProviderConfig from PROVIDER_DEFAULTS (no api_key — T-07-06)
- Fallback to app_settings defaults when load_provider_config returns None (D-15)
- Truncation delegated to provider._truncate() — no more text slices in classifier (D-12/D-13)
- Promote test_api_key_encrypt_decrypt to passing (round-trip + domain salt isolation)
- Update test_classifier.py: test_per_user_provider + test_default_provider_fallback use ProviderConfig assertions
Apply all available OS security updates in the runtime image stage to pull in
openssl 3.5.5-1~deb13u2 (fixes CVE-2026-31789 heap buffer overflow). Three
CVEs with no upstream fix (Mesa will_not_fix, perl-base affected) documented
and suppressed in .trivyignore with rationale. Trivy rescan exits 0 (D-10 gate
passes).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- test_get_provider_typed: registry get_provider() returns correct class + ValueError on bogus id
- test_client_singleton: AsyncOpenAI instantiated once per provider instance (D-07)
- test_generic_openai_json_mode: response_format present/absent based on supports_json_mode (D-01)
- test_context_chars_truncation: _truncate() shortens long text (D-12)
- test_smart_truncation: 60% head + 40% tail truncation strategy (D-13)
- test_gemini_fallback_to_parse_classification: supports_json_mode=False skips
response_format AND routes through parse_classification() from ai.utils (D-02 contract)
- test_anthropic_structured_output remains xfail (Plan 07-03)
- Rewrites ai/__init__.py with _REGISTRY dict (10 entries) replacing the if/elif chain
- get_provider() accepts ProviderConfig (not raw dict), raises ValueError on unknown
- Resolves effective values: api_key fallback to "not-needed", model/base_url/context_chars
from PROVIDER_DEFAULTS when config fields are empty/zero
- SUPPORTS_JSON_MODE lookup sets supports_json_mode on GenericOpenAIProvider instances
- AnthropicProvider instantiated without base_url (Plan 03 will widen the ctor)
- provider_config.py: context_chars default changed to 0 (sentinel for PROVIDER_DEFAULTS
resolution); 8000 default was inconsistent with O(1) factory pattern (Rule 1 fix)