- New backend/api/admin/overview.py: aggregate stats (user_count,
total_storage_bytes, doc_status) + last 10 audit entries in one payload
- Sub-router carries NO prefix (parent __init__.py carries /api/admin)
- Reuses _build_filtered_query_with_handles + _audit_to_dict_with_handles
from api.audit — no new serializer or query logic
- Registered overview_router on admin parent router in __init__.py
- All 8 ADMIN-11 tests pass; existing admin and audit suites stay green
- 8 xfail tests covering auth guards, aggregate stats, and sensitive field scan
- Tests will become real assertions once overview.py is implemented in Task 2
Task 3: 54 admin+cloud+ai-config tests pass with package; admin.py and
admin_OLD_REMOVE_IN_TASK_3.py removed. admin/ package is the sole definition.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove @pytest.mark.xfail decorator from test_change_password_revokes_other_sessions (CR-01)
- Remove @pytest.mark.xfail decorator from test_enable_totp_revokes_other_sessions (CR-02)
- Remove @pytest.mark.xfail decorator from test_disable_totp_revokes_other_sessions (CR-03)
- All three tests now show PASSED (not XPASS) in strict pytest runs
- test_change_password_revokes_other_sessions: verifies skip_token_hash
preserves current session while revoking all other refresh tokens
- test_enable_totp_revokes_other_sessions: verifies TOTP enrollment
revokes other sessions via skip_token_hash
- test_disable_totp_revokes_other_sessions: verifies TOTP disablement
revokes other sessions via skip_token_hash
- All three marked xfail(strict=False); currently xpassed (backend already complete)
- Plan 08-03 promotes these to strict passing tests
- New cross-package Pydantic schemas module (D-10, CODE-08)
- CloudConnectionOut moved here from api/admin.py to eliminate coupling
- SEC-08: credentials_enc deliberately excluded; 7-field whitelist preserved
- field_validator coerce_id_to_str preserves UUID→str coercion behavior
- Original definition in api/admin.py left intact until plan 08-04
Without a separator, UA='foobar'+AL='' and UA='foo'+AL='bar' produced the same
HMAC input, allowing a token fingerprint to match a different header split.
Fix: use '\x00' as separator between user_agent and accept_lang fields.
Regression test added: test_fgp_no_concatenation_collision (FGP-05).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- api/auth.py login + refresh call sites pass User-Agent and Accept-Language
headers to create_access_token (D-05 — fgp binding at token issuance)
- test_auth_fgp.py: promote all 4 xfail stubs to real assertions (FGP-01..04)
- conftest.py: add _TEST_USER_AGENT constant; configure async_client to send
consistent User-Agent; update auth_user/second_auth_user/admin_user fixtures
to bind fgp to _TEST_USER_AGENT so tokens validate correctly in tests
- test_auth_deps.py: import _TEST_USER_AGENT; update auth_client fixture and
all create_access_token calls to use the constant
- test_cloud.py: update _create_user_and_token to bind fgp to _TEST_USER_AGENT
- test_documents.py: update 3 inline create_access_token calls to pass user_agent
- test_security_headers.py: import _TEST_USER_AGENT; update headers_client +
token creation to use the constant
- Version bump: backend 0.1.2 → 0.1.3, frontend 0.1.2 → 0.1.3
- [Rule 1 - Bug] Fix httpx default User-Agent vs empty-string fgp mismatch in
test infrastructure: 10 tests were failing due to fgp check rejecting tokens
created with fgp="" when client sent "python-httpx/X.Y.Z"
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
- 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>
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>
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>
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>