Commit Graph
100 Commits
Author SHA1 Message Date
curo1305andClaude Sonnet 4.6 1eb1d59c8d docs(phase-07.3): update tracking after wave 0
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 16:54:48 +02:00
curo1305 b9e1f2a464 chore: merge executor worktree (worktree-agent-a6bcbfd85b592800f) 2026-06-06 16:53:02 +02:00
curo1305 b1e3d0bc73 docs(07.3-01): add plan execution SUMMARY.md
- 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
2026-06-06 16:51:40 +02:00
curo1305 fac0e781f9 test(07.3-01): extend test_settings_has_jwt_config — red until Plan 02
- Assert settings.refresh_token_expire_hours == 16 (CFG-01 / D-09)
- Assert hasattr(settings, "jwt_private_key") (CFG-01 / D-01)
- Assert hasattr(settings, "jwt_public_key") (CFG-01 / D-01)
- Existing refresh_token_expire_days == 30 assertion preserved (Pitfall 5)
- Test intentionally fails until Plan 02 adds the three new config fields
2026-06-06 16:50:32 +02:00
curo1305 5c1a1f9504 test(07.3-01): add Wave 0 xfail scaffold — 9 ES256/RM/CFG stubs
- Create backend/tests/test_auth_es256.py with 9 xfail(strict=True) stubs
- Cover ES256-01..05 (access token, HS256 rejection, reset token, startup rotation)
- Cover RM-01..03 (16h default TTL, 30d remember_me TTL, cookie max_age)
- Cover CFG-01 satellite (jwt_private_key/jwt_public_key presence)
- Add es256_keys autouse fixture generating fresh P-256 keypair per test
- All 9 stubs collect and run as XFAIL; zero XPASS, zero ERROR
2026-06-06 16:49:58 +02:00
curo1305andClaude Sonnet 4.6 4d0ada6d06 docs(phase-07.2): mark VERIFICATION.md passed — CR-02 gap resolved
Gap (password_reset_confirm missing user_nbf write) was fixed during UAT
phase: request: Request param added + Redis write at auth.py:744-750.
VALIDATION.md: 84/84 tests, nyquist_compliant=true. VERIFICATION.md
updated from gaps_found → passed (9/9). STATE.md advanced to phase 07.3.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 12:37:59 +02:00
curo1305andClaude Sonnet 4.6 2265aaaba4 docs(phase-07.2): mark VALIDATION.md complete — nyquist_compliant: true
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>
2026-06-06 11:54:57 +02:00
curo1305andClaude Sonnet 4.6 b8e0e3adec test(07.2): complete automated UAT — 7/7 pass
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>
2026-06-06 11:47:59 +02:00
curo1305andClaude Sonnet 4.6 95c6db5c42 security(07.2): add SECURITY.md audit — JTI claim + Redis NBF revocation
9/9 threats CLOSED; 11/11 Phase 7.2 tests PASSED; bandit 0 HIGH; full
391-test suite green. Documents gap closure for CR-02 (password_reset_confirm
user_nbf write). HS256→ES256 deferred to Phase 7.3.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 00:33:31 +02:00
curo1305 646736d4d3 docs(phase-07.2): complete phase execution 2026-06-06 00:12:09 +02:00
curo1305 8e4e199062 docs(07.2): add phase verification report 2026-06-06 00:11:44 +02:00
curo1305 d3deef4f95 fix(07.2): add user_nbf write to password_reset_confirm — CR-02 gap closure 2026-06-06 00:11:41 +02:00
curo1305 7e23a3ed0e docs(07.2): add code review report 2026-06-06 00:07:51 +02:00
curo1305 a20d27123d docs(07.2): update tracking after Wave 2 — plan 07.2-03 complete 2026-06-06 00:04:58 +02:00
curo1305 d3588ba055 docs(07.2-03): complete Wave 2 plan — user_nbf write sites in all 4 security-event handlers SUMMARY 2026-06-06 00:04:45 +02:00
curo1305 ae11a6e913 security(07.2-03): add import time + user_nbf write to admin deactivation handler; promote stubs to PASSED 2026-06-06 00:02:27 +02:00
curo1305 efeb75b279 feat(07.2-03): add import time + user_nbf writes to api/auth.py (change_password, enable_totp, disable_totp) 2026-06-06 00:01:19 +02:00
curo1305 2665a5085d docs(07.2): update tracking after Wave 1 — plan 07.2-02 complete 2026-06-05 19:21:48 +02:00
curo1305 9f90d46649 chore: merge executor worktree (worktree-agent-a9a1eb8685f2efc98) — Wave 1 plan 07.2-02 2026-06-05 19:21:09 +02:00
curo1305 61199752b5 docs(07.2-02): complete Wave 1 plan — jti claim + user_nbf NBF check SUMMARY
- jti UUID claim in create_access_token (D-01)
- user_nbf Redis NBF check in get_current_user with fail-open + re-raise guard (D-02, D-03, D-04, T-7.2-02)
- 4 Wave 0 xfail stubs promoted to PASS; 29 tests passing in target suites
2026-06-05 19:20:42 +02:00
curo1305 30ad9fd015 security(07.2-02): add user_nbf Redis NBF check to get_current_user
- Added import logging + _logger = logging.getLogger(__name__) to deps/auth.py
- Inserted user_nbf Redis check block after decode_access_token in get_current_user
- Check reads user_nbf:{payload['sub']} from Redis; rejects if payload['iat'] < stored timestamp
- Uses strict < comparison (token issued exactly at event second is allowed — D-02)
- Handles both bytes and str returns from Redis for FakeRedis/aioredis compatibility
- except HTTPException: raise guard precedes broad except Exception (T-7.2-02 Pitfall 1)
- Broad except logs warning and fails open (D-04 — Redis outage must not block requests)
- Promoted all 3 Wave 0 NBF xfail stubs to passing assertions in test_auth_deps.py
2026-06-05 19:19:06 +02:00
curo1305 c00c1fbaa7 feat(07.2-02): add jti UUID claim to create_access_token
- 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
2026-06-05 19:15:01 +02:00
curo1305 a80b632ac8 docs(07.2): update tracking after Wave 0 — plan 07.2-01 complete 2026-06-05 19:12:41 +02:00
curo1305 8d44018f40 chore: merge executor worktree (worktree-agent-a2f182fa26b9780c8) — Wave 0 plan 07.2-01 2026-06-05 19:11:20 +02:00
curo1305 f0ba9e6d8e docs(07.2-01): complete Wave 0 test scaffolding plan — SUMMARY
- FakeRedis mounted on test_auth_deps and admin_client fixtures
- 9 xfail(strict=False) stubs covering all Phase 7.2 behaviors
- 73 baseline tests pass, zero regressions
2026-06-05 19:10:03 +02:00
curo1305 eb1647293f test(07.2-01): add FakeRedis to admin_client + NBF-write xfail stubs for deactivation
- Import FakeRedis from tests.test_auth_api (no redefinition)
- Mount app.state.redis = FakeRedis() in admin_client fixture before yield
- Reset app.state.redis = None in teardown (mirrors authed_client convention)
- Add test_deactivate_user_writes_user_nbf_to_redis (xfail strict=False)
- Add test_activate_user_does_not_write_user_nbf (negative-guard, xfail strict=False)
- All 22 existing admin tests unaffected (zero regressions)
2026-06-05 19:08:27 +02:00
curo1305 097cdcadf8 test(07.2-01): add NBF-write xfail stubs for change_password/enable_totp/disable_totp
- 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
2026-06-05 19:07:30 +02:00
curo1305andClaude Sonnet 4.6 b7994efd06 docs(07.3): create phase plan — ES256 algorithm upgrade
3 plans (3 waves): Wave 0 xfail stubs, Wave 1 ES256 core + startup
rotation, Wave 2 remember-me TTL split + LoginView checkbox.
Verification passed (0 blockers, 1 minor doc warning fixed).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 19:07:25 +02:00
curo1305 c686d90e1f test(07.2-01): add JTI claim xfail stub to test_task2_auth_service.py
- 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
2026-06-05 19:06:22 +02:00
curo1305 49c63337db test(07.2-01): mount FakeRedis on test_auth_deps app + add NBF-check xfail stubs
- Import FakeRedis from tests.test_auth_api (no redefinition)
- Set test_app.state.redis = FakeRedis() in make_test_app() (Pitfall 4 guard)
- Add xfail(strict=False) stubs: reject iat<nbf, allow iat>nbf, fail-open on Redis error
- All 7 existing auth-dep tests unaffected (zero regressions)
2026-06-05 19:05:55 +02:00
curo1305andClaude Sonnet 4.6 7ee87c001b docs(07.2): create phase plan — JTI claim + Redis NBF access-token revocation
3 plans (Wave 0 test scaffolding, Wave 1 jti+NBF check, Wave 2 user_nbf
writes), VALIDATION.md, RESEARCH.md with resolved open questions.
Checker: 0 blockers, 1 warning resolved (RESEARCH.md open questions marked).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 18:56:49 +02:00
curo1305andClaude Sonnet 4.6 e9b2d88ba0 docs(07.3): research phase — ES256 algorithm upgrade, startup rotation, remember-me TTL
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 15:00:08 +02:00
curo1305andClaude Sonnet 4.6 35ff0d52fa docs(07.2): research phase — JTI claim + Redis access-token revocation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 14:57:11 +02:00
curo1305andClaude Sonnet 4.6 eda91f7512 test(load): add Locust load test result CSVs
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>
2026-06-05 14:44:59 +02:00
curo1305andClaude Sonnet 4.6 6ec2748b84 docs(readme): add project README for v0.1 alpha
Covers features, architecture diagram, token flow, stack, environment
variables, quick-start, running tests, cloud backend setup, AI provider
configuration, and alpha status disclaimer.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 14:44:54 +02:00
curo1305andClaude Sonnet 4.6 14c1bf437b docs(milestone): add v0.1 milestone audit report
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>
2026-06-05 14:44:50 +02:00
curo1305andClaude Sonnet 4.6 18de84d1a9 docs(phase-06): add pattern map and code review fix report
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>
2026-06-05 14:44:38 +02:00
curo1305andClaude Sonnet 4.6 1fd7395893 docs(state): clean up HANDOFF.json and mark all v1.0 phases complete
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>
2026-06-05 14:44:31 +02:00
curo1305 77135df803 docs(07.3): capture phase context 2026-06-05 14:42:05 +02:00
curo1305 50b9ffc1f1 docs(state): record phase 7.2 context session 2026-06-05 14:37:55 +02:00
curo1305 8c29af7f90 docs(07.2): capture phase context 2026-06-05 14:37:50 +02:00
curo1305andClaude Sonnet 4.6 c9fe69db3a docs(phase-07.1): add Nyquist VALIDATION.md — nyquist_compliant: true
All 6 tasks covered: 3 backend integration tests (pytest, green) + 3 frontend
unit tests (vitest, green). 0 manual-only. Gap audit resolved 3/3 missing gaps.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 12:54:19 +02:00
curo1305andClaude Sonnet 4.6 8f8bfa5539 test(phase-07.1): add Nyquist validation tests for sessions_revoked frontend behavior
3 positive + 2 negative Vitest tests covering CR-01/02/03 toast UX:
- SettingsAccountTab: toast appears/hidden after changePassword (CR-01)
- SettingsAccountTab: toast appears after disableTotp (CR-03)
- TotpEnrollment: inline alert appears/hidden after enable_totp (CR-02)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 12:54:14 +02:00
curo1305andClaude Sonnet 4.6 89375e6d93 docs(phase-07): mark VALIDATION.md complete — 10/11 tests green, nyquist_compliant
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>
2026-06-05 12:50:07 +02:00
curo1305andClaude Sonnet 4.6 c38c6b1c01 feat(07.1): session revocation on privilege change — CR-01/CR-02/CR-03
- revoke_all_refresh_tokens: add skip_token_hash optional param (exclude
  current session while revoking others)
- change_password, enable_totp, disable_totp: call revoke with skip hash
  derived from refresh cookie; return sessions_revoked in response and
  write to audit log metadata_
- 3 new tests: test_{change_password,enable_totp,disable_totp}_revokes_other_sessions
  — all PASSED; 373 total passing, 0 regressions
- Frontend toasts: SettingsAccountTab + TotpEnrollment show
  "Other sessions have been terminated." when sessions_revoked > 0
- Companion fixes: rate_limiting get_client_ip refactor, deps/auth.py
  request.state.current_user, locustfile refresh-token task removal
- Version bump: 0.1.0 → 0.1.1

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 12:47:16 +02:00
curo1305andClaude Sonnet 4.6 8d060a5da4 docs(state): mark Phase 7.1 planned — ready to execute
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 12:32:12 +02:00
curo1305andClaude Sonnet 4.6 a3a97430a2 docs(07.1): create phase plan — 2 waves, CR-01..03 session revocation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 12:27:24 +02:00
curo1305andClaude Sonnet 4.6 0fa23f5211 fix(auth): catch IntegrityError on register race → 409 + locustfile pre-auth rewrite
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>
2026-06-05 12:25:04 +02:00
curo1305 3f0e2ab44c docs(state): record phase 7.1 context session 2026-06-05 11:23:16 +02:00
curo1305 a7ee4fbd23 docs(07.1): capture phase context 2026-06-05 11:23:07 +02:00
curo1305andClaude Sonnet 4.6 d86664d3f7 docs(phase-6): mark validation nyquist_compliant — 13/13 tests green
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>
2026-06-05 11:10:15 +02:00
curo1305andClaude Sonnet 4.6 8629bc0854 docs(phase-7): add security threat verification — 12/12 CLOSED
Formal Phase 7 SECURITY.md:
- T-07-01: api_key_enc excluded from GET /api/admin/ai-config (whitelist)
- T-07-02: HKDF domain separation confirmed (ai-provider-settings vs cloud-credentials)
- T-07-03: is_active atomic UPDATE, no read-then-write
- T-07-04: empty api_key normalised to "not-needed" before AsyncOpenAI
- T-07-05/07/09/11: accepted risks documented
- T-07-06/08/10/12: mitigations verified in implementation + tests
bandit: zero HIGH; npm audit: zero high/critical; threats_open: 0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 10:50:44 +02:00
curo1305andClaude Sonnet 4.6 1a625ec365 docs(state): mark Phase 7 complete — v1.0 milestone done
All 7 phases complete. Phase 7 gates:
- pytest: 347 passed / 1 pre-existing failure (test_extract_docx)
- bandit: zero HIGH findings
- npm audit: 2 moderate dev-only (esbuild/vite) — zero high/critical
- UAT: 11/11 passed (07-UAT.md)

Stale HANDOFF.json removed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 10:44:24 +02:00
curo1305andClaude Sonnet 4.6 e58cb1eb01 docs(07-uat): mark all 11 UAT tests pass — phase 07 complete
Tests 8-11 verified:
- 8: classification_failed badge (status field fix + regression test)
- 9: Re-analyze button flow (code + Celery dispatch verified)
- 10: Celery retry exhaustion (max_retries=3, 30/90/270s backoff, _mark_classification_failed)
- 11: Non-admin 403 on PUT /api/admin/ai-config (get_current_admin dep)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 09:30:41 +02:00
curo1305andClaude Sonnet 4.6 76ebc3e96e fix(07): add status field to _doc_to_dict + regression test
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>
2026-06-05 09:30:03 +02:00
curo1305andClaude Sonnet 4.6 3b11b9a596 feat(07-uat): admin AI panel UX enhancements + DocumentCard status indicators
- 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>
2026-06-05 09:26:02 +02:00
curo1305andClaude Sonnet 4.6 ac2dded35b fix(celery+lmstudio): PYTHONPATH for forked workers + qwen3.5-9b thinking model support
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>
2026-06-05 00:00:26 +02:00
curo1305andClaude Sonnet 4.6 ca43e653aa fix(07): replace autodiscover_tasks with explicit imports — tasks/document_tasks not at tasks.tasks path
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 23:35:15 +02:00
curo1305 1f0808c303 docs(07-05): add plan summary — Tasks 1-3 complete, Task 4 human checkpoint pending
- Backend GET/PUT /api/admin/ai-config + test-connection + load_provider_config_by_id
- Frontend getAiConfig/saveAiConfig/testAiConnection helpers + AdminAiConfigTab system section
- DocumentCard classification_failed badge + Re-analyze button
- 131 frontend tests pass, 369 backend tests pass
2026-06-04 23:24:09 +02:00
curo1305 c45d9e470d feat(07-05): DocumentCard classification_failed badge + Re-analyze button + Vitest coverage
- Add red "Classification failed" pill badge when doc.status === 'classification_failed'
- Re-analyze button calls classifyDocument(doc.id) via @click.stop, emits 'reclassified'
- Spinner text "Re-analyzing…" while in flight; resets after 500ms (via setTimeout)
- Existing is_shared badge block preserved unchanged
- Create frontend/tests/DocumentCard.spec.js: 4 Vitest tests covering badge render,
  no-badge for ready/processing, and reanalyze() calling classifyDocument + emit
- 131 frontend tests pass, build exits 0 (D-11 frontend coverage per CLAUDE.md)
2026-06-04 23:23:12 +02:00
curo1305 0db412d66c feat(07-05): frontend AI config client helpers + AdminAiConfigTab system section + Vitest tests
- Add getAiConfig/saveAiConfig/testAiConnection to frontend/src/api/client.js
- AdminAiConfigTab.vue: add System AI Providers section above existing per-user table
  - Per-provider accordion (10 providers from PROVIDER_DEFAULTS)
  - Write-only API key field (never pre-filled), base URL, model, context_chars inputs
  - Set Active (atomic flip), Save, Test Connection buttons with inline badges
  - Existing per-user ai-config table and saveConfig logic untouched (Pitfall 6)
- Create frontend/tests/api.spec.js: 4 Vitest tests for getAiConfig/saveAiConfig/testAiConnection
- All 127 frontend tests pass, build exits 0 (D-11 frontend coverage)
2026-06-04 23:22:08 +02:00
curo1305 e678930b8d feat(07-05): admin AI-config endpoints + load_provider_config_by_id + atomic is_active flip
- Add GET/PUT /api/admin/ai-config + GET /api/admin/ai-config/test-connection
- _ai_config_to_dict whitelist excludes api_key_enc (T-07-01)
- PUT: HKDF-encrypt api_key, atomic UPDATE SET is_active = (provider_id = target) (T-07-03)
- PUT: upsert with PROVIDER_DEFAULTS for omitted fields; audit log records fields_changed only (T-07-14)
- All 3 endpoints require get_current_admin (T-07-15)
- Add load_provider_config_by_id to services/ai_config.py (ignores is_active)
- Promote 3 xfail tests in test_admin_ai_config.py — all 3 pass
2026-06-04 23:19:40 +02:00
curo1305andClaude Sonnet 4.6 fb4ce293ae fix(06): CR-07 update test to use event_type prefix instead of full type
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>
2026-06-04 23:15:49 +02:00
curo1305andClaude Sonnet 4.6 21366bd288 fix(06): WR-09 show error message when audit log fetch fails
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>
2026-06-04 23:15:49 +02:00
curo1305andClaude Sonnet 4.6 7cd29e9454 fix(06): WR-07 replace print() with structlog for cloud delete errors
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>
2026-06-04 23:15:49 +02:00
curo1305andClaude Sonnet 4.6 b0d2406acd fix(06): WR-04 add 10.0.0.0/8 to trusted proxy CIDR list
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>
2026-06-04 23:15:48 +02:00
curo1305andClaude Sonnet 4.6 013802aa74 fix(06): WR-03 fix locust load test document list shape mismatch
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>
2026-06-04 23:15:48 +02:00
curo1305andClaude Sonnet 4.6 4a5719311b fix(06): WR-01 reset auth_limiter between tests to prevent 429 cross-contamination
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>
2026-06-04 23:15:48 +02:00
curo1305andClaude Sonnet 4.6 10970d9557 fix(06): CR-07 validate event_type against allowlist before LIKE filter
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>
2026-06-04 23:15:48 +02:00
curo1305andClaude Sonnet 4.6 a37a91071c fix(06): CR-06 emit request_complete log line with duration_ms and status_code
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>
2026-06-04 23:15:48 +02:00
curo1305andClaude Sonnet 4.6 aad7635623 fix(06): CR-05 hash attempted email in audit log to avoid PII storage
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>
2026-06-04 23:15:48 +02:00
curo1305andClaude Sonnet 4.6 3a6251ca23 fix(06): CR-04 add allowlist validation for default_storage_backend
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>
2026-06-04 23:15:48 +02:00
curo1305andClaude Sonnet 4.6 23c27efd65 fix(06): CR-03 replace raw X-Forwarded-For reads with get_client_ip()
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>
2026-06-04 23:15:48 +02:00
curo1305andClaude Sonnet 4.6 a8dbb02ff0 fix(06): CR-01 CR-02 WR-02 WR-05 WR-06 WR-08 docker-compose security hardening
- 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>
2026-06-04 23:15:48 +02:00
curo1305andClaude Sonnet 4.6 ee4df4537d docs(phase-7): update tracking after wave 4 — plan 07-04 complete
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 23:14:08 +02:00
curo1305 beb438f113 chore: merge executor worktree (worktree-agent-a707cbd4c3780710b) 2026-06-04 23:12:48 +02:00
curo1305andClaude Sonnet 4.6 0c1ae5284f docs(07-04): add plan summary — Celery retry harness + POST /classify re-queue complete
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 23:12:11 +02:00
curo1305 63cd707d52 feat(07-04): POST /classify re-queues Celery, removes synchronous classify (D-11)
- 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)
2026-06-04 23:10:49 +02:00
curo1305 e9ee5d4ba5 feat(07-04): Celery retry harness + _ClassificationError sentinel (D-09/D-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)
2026-06-04 23:08:16 +02:00
curo1305andClaude Sonnet 4.6 888856aa8b docs(06): add code review report — 7 critical, 9 warning, 4 info findings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 23:01:53 +02:00
curo1305andClaude Sonnet 4.6 b8b0840729 docs(phase-7): update tracking after wave 3 — plan 07-03 complete
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 19:19:03 +02:00
curo1305 e0cd183a63 chore: merge executor worktree (wave3-recovery/07-03) 2026-06-04 19:17:50 +02:00
curo1305 5f72814f07 docs(07-03): complete plan summary — AnthropicProvider output_config + classifier ProviderConfig refactor 2026-06-04 19:16:43 +02:00
curo1305 95c386f764 feat(07-03): classifier wired to load_provider_config + ai_config stub removed — D-04/D-06
- 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
2026-06-04 19:14:48 +02:00
curo1305andClaude Sonnet 4.6 420fecdacd docs(phase-6): update tracking after wave 3 — plan 06-06 complete, Phase 6 done
All 6 Phase 6 plans complete. Phase 6 status updated to Complete in STATE.md
and ROADMAP.md (plans 06-04, 06-05, 06-06 checked). UAT passed (06-UAT.md),
D-10 CVE gate passed (trivy exit 0), D-14 RUNBOOK.md in place.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 19:14:22 +02:00
curo1305andClaude Sonnet 4.6 54521e2b99 docs(06-06): create plan SUMMARY — RUNBOOK.md + D-10 CVE gate passed
Task 1: RUNBOOK.md (580 lines, 8 D-14 sections). Task 2: trivy scan exits 0
after patching OpenSSL CVE-2026-31789; Mesa/perl unfixable CVEs documented in
.trivyignore. Phase 6 plan 06-06 complete.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 19:13:45 +02:00
curo1305andClaude Sonnet 4.6 382f9bec6b fix(06-06): patch OpenSSL CVE-2026-31789 — add apt-get upgrade to runtime Dockerfile stage
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>
2026-06-04 19:13:05 +02:00
curo1305 efc177a155 feat(07-03): AnthropicProvider singleton + output_config + truncation — D-03/D-07/D-12/D-13
- Delete MAX_AI_CHARS constant and _client() method from anthropic_provider.py
- Add self._client = AsyncAnthropic(...) singleton in __init__ (D-07)
- Add _truncate() with 60/40 split using self._context_chars (D-13)
- Add _CLASSIFICATION_SCHEMA and _SUGGESTIONS_SCHEMA module-level constants
- classify() and suggest_topics() pass output_config with json_schema format (D-03)
- stop_reason != "end_turn" degrades to parse_classification("") (T-07-08)
- Widen __init__ signature to (api_key, model, context_chars, base_url) (uniform ctor)
- Update ai/__init__.py to pass context_chars + base_url to AnthropicProvider
- Promote test_anthropic_structured_output + add test_anthropic_stop_reason_fallback
2026-06-04 19:10:05 +02:00
curo1305andClaude Sonnet 4.6 ea8df02491 docs(phase-7): update tracking after wave 2 — plan 07-02 complete
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 19:06:07 +02:00
curo1305andClaude Sonnet 4.6 670df192d6 docs(06-06): add RUNBOOK.md — env vars, startup, backup, health checks, escalation, failure modes
Covers D-14: all required env vars with examples, Docker Compose startup/shutdown
procedures, PostgreSQL + MinIO backup strategy with restore commands, health check
verification for all 7 services, on-call escalation for 6 alert classes, and
recovery steps for all 7 Phase 6 failure modes (Pitfalls 1, 5, 6, 7 + rate limit
edge cases + Grafana/Loki). docker scout CVE scan gate (D-10) documented in
Section 5 with prerequisites, pass/fail criteria, remediation, and Trivy fallback.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 19:05:05 +02:00
curo1305 60b621f424 chore: merge executor worktree (worktree-agent-afe278fe2f809d30b) 2026-06-04 19:04:52 +02:00
curo1305 00c154d61b docs(07-02): complete provider config + GenericOpenAIProvider + registry plan
- ProviderConfig + PROVIDER_DEFAULTS + SUPPORTS_JSON_MODE in provider_config.py
- GenericOpenAIProvider with JSON-mode conditional + D-02 fallback
- OpenAIProvider singleton _client + smart truncation (D-07/D-13)
- Registry-based get_provider(config: ProviderConfig) — no if/elif chain
- MAX_AI_CHARS removed from openai_provider.py + classifier.py
- anthropic floor bumped to >=0.95.0 for Plan 03 output_config
- 6 Wave-2 xfail tests promoted; suite: 363 passed, 15 xfailed, 1 pre-existing failure
2026-06-04 19:04:18 +02:00
curo1305 209b156af1 test(07-02): promote 6 Wave-2 xfail tests to passing — D-01/D-02/D-07/D-12/D-13
- 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)
2026-06-04 19:01:38 +02:00
curo1305 13eef371bc feat(07-02): registry-based get_provider(config: ProviderConfig) — D-06
- 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)
2026-06-04 19:01:26 +02:00
curo1305andClaude Sonnet 4.6 582136c120 test(06): complete UAT - 8 passed, 0 issues
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 19:00:19 +02:00
curo1305 02bcbb9143 feat(07-02): singleton OpenAIProvider + GenericOpenAIProvider + MAX_AI_CHARS removal
- openai_provider.py: singleton self._client=AsyncOpenAI(...) in __init__ (D-07),
  _truncate() 60/40 smart truncation (D-13), removed MAX_AI_CHARS constant,
  changed __init__ signature to include context_chars, removed _client() method
- generic_openai_provider.py: new class, subclasses OpenAIProvider, conditional
  response_format={"type":"json_object"} on supports_json_mode flag (D-01/D-02),
  imports parse_classification + parse_suggestions from ai.utils (D-02 contract)
- ollama_provider.py: added context_chars kwarg with default 8000, passes through
- lmstudio_provider.py: added context_chars kwarg with default 8000, passes through
- classifier.py: removed MAX_AI_CHARS constant and text[:MAX_AI_CHARS] slices;
  truncation now handled inside each provider via _truncate()
- requirements.txt: bumped anthropic floor to >=0.95.0 (D-03 output_config support)
2026-06-04 18:58:19 +02:00
curo1305 beb5b5e49d feat(07-02): ProviderConfig Pydantic model + PROVIDER_DEFAULTS + SUPPORTS_JSON_MODE
- Creates backend/ai/provider_config.py as a pure data module (no provider imports)
- ProviderConfig(BaseModel) with extra=forbid: provider_id, api_key, base_url, model, context_chars
- PROVIDER_DEFAULTS dict with all 10 providers and verbatim base_url/model/context_chars from RESEARCH.md
- SUPPORTS_JSON_MODE dict with gemini=False per JSON Compatibility Matrix (D-02)
2026-06-04 18:56:31 +02:00
curo1305andClaude Sonnet 4.6 651713fa7a docs(phase-7): update tracking after wave 1 — plan 07-01 complete
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 18:53:25 +02:00
curo1305 88f9175f28 chore: merge executor worktree (worktree-agent-a034ce22177165198) 2026-06-04 18:51:55 +02:00
curo1305 7986661333 docs(07-01): complete plan summary — system_settings DB + HKDF + Wave 0 stubs
Tasks: 3/3 complete. Commits: 4febe2f, 4eb3177, 0fd6930.
Created system_settings table (Alembic 0005), SystemSettings ORM, HKDF AI key
encryption (info=b"ai-provider-settings"), load_provider_config(), seed startup
hook, and 13 Wave 0 xfail stubs across 4 new test files.
2026-06-04 18:51:12 +02:00