diff --git a/.planning/phases/07-redo-and-optimize-llm-integration/07-01-SUMMARY.md b/.planning/phases/07-redo-and-optimize-llm-integration/07-01-SUMMARY.md new file mode 100644 index 0000000..d15ae8b --- /dev/null +++ b/.planning/phases/07-redo-and-optimize-llm-integration/07-01-SUMMARY.md @@ -0,0 +1,155 @@ +--- +phase: 07-redo-and-optimize-llm-integration +plan: "01" +subsystem: backend/ai-config +tags: + - database + - encryption + - hkdf + - testing + - wave-0-scaffold +dependency_graph: + requires: + - "06-05 (trusted-proxy, per-account rate limiting)" + provides: + - "system_settings table (Alembic 0005)" + - "SystemSettings ORM model" + - "HKDF encryption helpers for AI API keys" + - "load_provider_config() DB reader" + - "seed_system_settings_from_env() startup hook" + - "Wave 0 xfail test stubs (13 new stubs)" + affects: + - "07-02 (ProviderConfig + GenericOpenAIProvider — depends on system_settings table)" + - "07-03 (Anthropic output_config — depends on load_provider_config)" + - "07-05 (Admin AI panel — depends on system_settings table)" +tech_stack: + added: [] + patterns: + - "HKDF-SHA256 with info=b\"ai-provider-settings\" (domain-separated from b\"cloud-credentials\")" + - "Fresh HKDF instance per call (AlreadyFinalized guard)" + - "Fernet symmetric encryption for API keys (no JSON wrapping)" + - "Lazy import of ProviderConfig inside load_provider_config() to avoid Plan 01/02 circular dep" + - "Try/except lifespan seed to survive pre-migration fresh container startup" +key_files: + created: + - backend/migrations/versions/0005_system_settings.py + - backend/services/ai_config.py + - backend/tests/test_ai_providers.py + - backend/tests/test_ai_config.py + - backend/tests/test_admin_ai_config.py + - backend/tests/test_document_tasks.py + modified: + - backend/db/models.py + - backend/main.py + - backend/tests/test_documents.py +decisions: + - "HKDF info=b\"ai-provider-settings\" enforces domain separation from cloud-credentials — same master key produces different derived Fernet keys" + - "Fresh HKDF instance per _derive_ai_settings_key() call (cryptography AlreadyFinalized invariant)" + - "_ProviderConfigStub introduced as stub until Plan 02 creates ai/provider_config.py; removed in Plan 03" + - "seed_system_settings_from_env wrapped in try/except in lifespan — pre-migration container startup must not crash" + - "UniqueConstraint uq_system_settings_provider_id on provider_id enforces one row per provider" + - "D-14 regression guard: docker-compose.yml retains 3 host-gateway entries (backend, celery-worker, celery-worker-two); all >= required 2" +metrics: + duration: "~25 minutes" + completed: "2026-06-04" + tasks_completed: 3 + tasks_total: 3 + files_created: 6 + files_modified: 3 +--- + +# Phase 7 Plan 01: Database, Encryption, and Test Scaffolding Foundation Summary + +HKDF/Fernet encryption layer for AI provider API keys stored in a new `system_settings` DB table; Wave 0 xfail test stubs providing the full Phase 7 testing skeleton; startup seed from env vars. + +## Tasks Completed + +| Task | Description | Commit | Files | +|------|-------------|--------|-------| +| 1 | Wave 0 xfail stubs — 13 new stubs across 4 new files + 1 append | 4febe2f | test_ai_providers.py, test_ai_config.py, test_admin_ai_config.py, test_document_tasks.py, test_documents.py | +| 2 | Alembic migration 0005 + SystemSettings ORM model | 4eb3177 | migrations/versions/0005_system_settings.py, db/models.py | +| 3 | services/ai_config.py — HKDF helpers, loader, env seed + main.py wiring | 0fd6930 | services/ai_config.py, main.py | + +## What Was Built + +### Task 1: Wave 0 Test Scaffold + +Created four new test files and appended one stub to an existing file: + +- `test_ai_providers.py` — 7 stubs: test_generic_openai_json_mode, test_anthropic_structured_output, test_get_provider_typed, test_client_singleton, test_context_chars_truncation, test_smart_truncation, test_gemini_fallback_to_parse_classification +- `test_ai_config.py` — 2 stubs: test_load_provider_config, test_api_key_encrypt_decrypt +- `test_admin_ai_config.py` — 2 stubs: test_get_never_returns_key, test_put_writes_active_provider +- `test_document_tasks.py` — 2 stubs: test_retry_backoff, test_exhaustion_sets_failed_status +- `test_documents.py` — 1 appended stub: test_reclassify_requeues_celery + +All stubs use `@pytest.mark.xfail(strict=False, reason="Wave 0 stub — promoted in Plan 07-XX")` per the xfail(strict=False) STATE.md decision. Bodies are single `pytest.xfail()` calls only. + +D-14 regression guard verified: `docker-compose.yml` contains 3 `host.docker.internal:host-gateway` entries (backend + celery-worker + celery-worker-two), satisfying the minimum-2 requirement. + +### Task 2: Alembic Migration 0005 + SystemSettings ORM + +`backend/migrations/versions/0005_system_settings.py` creates the `system_settings` table with: +- `id`: UUID PK with `gen_random_uuid()` server default +- `provider_id`: String NOT NULL UNIQUE (uq_system_settings_provider_id constraint) +- `api_key_enc`: Text NULL (Fernet-encrypted; NULL for local providers like Ollama) +- `base_url`: Text NULL +- `model_name`: Text NOT NULL, default '' +- `context_chars`: Integer NOT NULL, default 8000 +- `is_active`: Boolean NOT NULL, default false +- `created_at` / `updated_at`: TIMESTAMPTZ NOT NULL, default now() + +`backend/db/models.py` gains the `SystemSettings(Base)` ORM model with identical column declarations using `Mapped[]` type syntax, mirroring the CloudConnection pattern. + +### Task 3: services/ai_config.py + +Complete encryption + loader + seed module: + +- `_derive_ai_settings_key(master_key, provider_id)`: Creates FRESH HKDF on every call (AlreadyFinalized guard), `salt=provider_id.encode("utf-8")`, `info=b"ai-provider-settings"` (domain-separated from `b"cloud-credentials"`) +- `encrypt_api_key(master_key, provider_id, api_key)`: `Fernet.encrypt(api_key.encode()).decode()` — no JSON wrapping +- `decrypt_api_key(master_key, provider_id, api_key_enc)`: `Fernet.decrypt(...).decode()` — no JSON unwrapping +- `load_provider_config(session)`: Reads `is_active=True` row, decrypts API key, returns `_ProviderConfigStub` (upgraded to real `ProviderConfig` lazily once Plan 02 lands) +- `seed_system_settings_from_env(session)`: Inserts default provider row from `settings.default_ai_provider / default_ai_model` when no row exists for that provider_id; idempotent +- `_ProviderConfigStub`: Minimal Pydantic model stub; removed in Plan 03 + +`backend/main.py` updated to import and call `seed_system_settings_from_env` inside the async lifespan with `try/except` so that a missing table (pre-migration fresh container) logs a warning and skips rather than crashing. + +## Verification Results + +- Round-trip smoke test: `encrypt_api_key(mk, 'openai', 'sk-test')` → `decrypt_api_key(mk, 'openai', ct) == 'sk-test'` — PASS +- Domain salt isolation: ciphertext for `provider_id="openai"` vs `provider_id="anthropic"` differs — PASS +- AlreadyFinalized guard: `_derive_ai_settings_key()` called twice without error — PASS +- Domain separation: `info=b"ai-provider-settings"` present; `info=b"cloud-credentials"` absent from ai_config.py — PASS +- `SystemSettings.__table_args__` contains `UniqueConstraint(name="uq_system_settings_provider_id")` — PASS +- `from db.models import SystemSettings` exits 0 — PASS +- Full suite: 1 failed (pre-existing test_extract_docx), 357 passed, 21 xfailed — PASS (no new failures) + +## Deviations from Plan + +None — plan executed exactly as written. + +## Known Stubs + +- `_ProviderConfigStub` in `services/ai_config.py` lines 44-50: intentional temporary stub; Plan 02 creates `ai/provider_config.py` and `load_provider_config()` will import `ProviderConfig` from there via lazy import. + +## Threat Flags + +No new threat surface introduced beyond what is described in the plan's ``. The `system_settings` table is accessed only by the `seed_system_settings_from_env` startup hook and the `load_provider_config` service function. No new API endpoints or network-accessible paths are added in this plan. + +## Self-Check: PASSED + +Files created/modified: + +- [x] backend/migrations/versions/0005_system_settings.py — FOUND +- [x] backend/db/models.py — FOUND (SystemSettings class present) +- [x] backend/services/ai_config.py — FOUND +- [x] backend/main.py — FOUND (seed call present) +- [x] backend/tests/test_ai_providers.py — FOUND (7 xfail stubs) +- [x] backend/tests/test_ai_config.py — FOUND (2 xfail stubs) +- [x] backend/tests/test_admin_ai_config.py — FOUND (2 xfail stubs) +- [x] backend/tests/test_document_tasks.py — FOUND (2 xfail stubs) +- [x] backend/tests/test_documents.py — FOUND (test_reclassify_requeues_celery appended) + +Commits: +- [x] 4febe2f — test(07-01): Wave 0 xfail stubs +- [x] 4eb3177 — feat(07-01): Alembic migration + SystemSettings ORM +- [x] 0fd6930 — feat(07-01): services/ai_config.py + main.py wiring