Spaces:
Running
Running
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # AUTHORBOT RAG β PRODUCTION RULES ($500K BUDGET, ENTERPRISE SaaS) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # | |
| # READ THIS FILE BEFORE EVERY SESSION. NO EXCEPTIONS. | |
| # | |
| # These rules are derived from a line-by-line audit of the entire codebase. | |
| # Every rule exists because we found a gap, a vulnerability, or an inconsistency | |
| # that WILL cause problems at production scale. | |
| # | |
| # Format: Each rule has a WHY, WHERE, and ENFORCEMENT section. | |
| # Status: β = Implemented, β = NOT YET (must fix), β οΈ = Partial | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Β§1. AUTHENTICATION & AUTHORIZATION | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # R-001: Passwords MUST be hashed with bcrypt β₯12 rounds. | |
| # WHY: Lower rounds are brute-forceable with modern GPUs. | |
| # WHERE: app/services/auth_service.py β _hash_password() | |
| # STATUS: β Implemented (rounds=12) | |
| # R-002: Failed login MUST trigger account lockout (5 attempts β 15-min lock). | |
| # WHY: Prevents credential stuffing attacks. | |
| # WHERE: app/services/auth_service.py β login() | |
| # STATUS: β Implemented | |
| # R-003: Non-existent email MUST take constant time to respond. | |
| # WHY: Timing attacks can enumerate valid emails. | |
| # WHERE: app/services/auth_service.py:98-99 (dummy bcrypt comparison) | |
| # STATUS: β Implemented | |
| # R-004: JWT refresh MUST invalidate the old token (Redis blacklist by jti). | |
| # WHY: Stolen tokens remain valid until natural expiry without blacklisting. | |
| # WHERE: app/services/auth_service.py β refresh_tokens() | |
| # STATUS: β NOT IMPLEMENTED β old token still valid after refresh | |
| # R-005: Logout MUST add current JWT to Redis blacklist (TTL = remaining lifetime). | |
| # WHY: Cookie deletion is client-side only; server still accepts the token. | |
| # WHERE: app/api/schemas_router.py:78-81 | |
| # STATUS: β NOT IMPLEMENTED β only deletes cookie | |
| # R-006: Refresh token MUST use HttpOnly + Secure + SameSite=Lax. | |
| # WHY: Prevents XSS from stealing refresh tokens. | |
| # WHERE: app/api/schemas_router.py:26-35 | |
| # STATUS: β Implemented | |
| # R-007: SuperAdmin routes MUST require JWT + TOTP on EVERY request. | |
| # WHY: Single-factor auth on God-mode access = catastrophic if JWT stolen. | |
| # WHERE: app/dependencies.py β get_current_superadmin() | |
| # STATUS: β NOT IMPLEMENTED β only checks role, TOTP module exists but unused | |
| # R-008: Registration MUST validate email format (RFC 5322 regex or Pydantic EmailStr). | |
| # WHY: Invalid emails break notification system and create ghost accounts. | |
| # WHERE: app/services/auth_service.py β register() | |
| # STATUS: β NOT IMPLEMENTED β only lower().strip() | |
| # R-009: Registration MUST enforce password complexity (β₯8 chars, mixed case + digit). | |
| # WHY: Weak passwords on admin accounts = easy takeover. | |
| # WHERE: app/services/auth_service.py β register() | |
| # STATUS: β NOT IMPLEMENTED β no minimum length or complexity check | |
| # R-010: Password change MUST validate via Pydantic schema (not raw dict). | |
| # WHY: Raw dicts skip type coercion, field validation, and OpenAPI docs. | |
| # WHERE: app/admin/router.py:461-487 | |
| # STATUS: β NOT IMPLEMENTED β uses body: dict | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Β§2. SUBSCRIPTION & TOKEN SECURITY | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # R-011: Subscription token HMAC MUST use constant-time comparison. | |
| # WHY: Non-constant comparison leaks signature bytes via timing side-channel. | |
| # WHERE: app/core/access/token_crypto.py:179 | |
| # STATUS: β hmac.compare_digest() | |
| # R-012: Raw subscription tokens MUST never be stored β only SHA-256 hash. | |
| # WHY: DB breach exposes all tokens if stored in plaintext. | |
| # WHERE: app/core/access/token_crypto.py:199-210 | |
| # STATUS: β Implemented | |
| # R-013: Token validation MUST check all 5 layers (HMAC β Expiry β Redis β DB β Budget). | |
| # WHY: Skipping any layer creates a bypass. | |
| # WHERE: app/core/access/subscription.py:34-111 | |
| # STATUS: β Implemented | |
| # R-014: tokens_used MUST be incremented atomically on every chat turn. | |
| # WHY: Read-modify-write on ORM object loses increments under concurrency. | |
| # WHERE: app/services/token_budget.py:89 | |
| # STATUS: β NOT ATOMIC β uses Python addition instead of SQL atomic update | |
| # FIX: Use `update(ClientAccess).values(tokens_used=ClientAccess.tokens_used + N)` | |
| # R-015: budget_exhausted flag MUST be set at 100% usage. | |
| # WHY: Without it, exhausted authors continue using API at our cost. | |
| # WHERE: app/services/token_budget.py:92-93 | |
| # STATUS: β Redis flag set | |
| # R-016: BudgetExhaustedError MUST return HTTP 200 (not 403) with friendly message. | |
| # WHY: Visitors should see "Taking a short break" not "Budget exhausted" error. | |
| # WHERE: app/dependencies.py:210-211 overrides the global handler in main.py | |
| # STATUS: β BROKEN β dependency catches and re-raises as HTTPException(403) | |
| # FIX: Let BudgetExhaustedError propagate uncaught to the global handler. | |
| # R-017: Token budget formula MUST always be: total = token_budget + bonus_tokens. | |
| # WHY: Inconsistent formula = different numbers on admin vs superadmin vs embed. | |
| # WHERE: app/services/token_budget.py β total_budget() | |
| # STATUS: β Centralized in token_budget.py | |
| # R-018: Revocation MUST propagate to Redis blacklist immediately (O(1) check). | |
| # WHY: DB-only revocation has latency; widget keeps working for cached sessions. | |
| # WHERE: app/services/superadmin_service.py:308-312 | |
| # STATUS: β Implemented | |
| # R-019: chatbot_is_active=false MUST block all chat requests. | |
| # WHY: Suspended authors must not serve visitors. | |
| # WHERE: app/core/access/subscription.py:108-109 | |
| # STATUS: β Checked in validation chain | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Β§3. RATE LIMITING & DDoS PROTECTION | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # R-020: Chat endpoint rate limit: 60 req/min per IP. | |
| # WHERE: app/middleware/rate_limit_middleware.py:21 | |
| # STATUS: β Implemented | |
| # R-021: Auth endpoint rate limit: 10 req/min per IP. | |
| # WHERE: app/middleware/rate_limit_middleware.py:22 | |
| # STATUS: β Implemented | |
| # R-022: Rate limiter MUST fail-open (no Redis = allow traffic, not crash). | |
| # WHERE: app/middleware/rate_limit_middleware.py:61-62 | |
| # STATUS: β Implemented | |
| # R-023: SSE endpoints MUST have a max connection timeout (5 minutes). | |
| # WHY: Unbounded SSE = connection exhaustion attack vector. | |
| # WHERE: app/api/chat.py:252-274, app/api/ingest.py:90-116 | |
| # STATUS: β NOT IMPLEMENTED β runs forever until client disconnects | |
| # FIX: Wrap SSE loop in asyncio.timeout(300). Send heartbeat pings every 30s. | |
| # R-024: File upload MUST reject oversized files BEFORE reading into memory. | |
| # WHY: A 500MB upload = 500MB RAM consumption before any validation. | |
| # WHERE: app/api/ingest.py:43 | |
| # STATUS: β NOT IMPLEMENTED β file.read() loads everything first | |
| # FIX: Use chunked reads or set max_upload_size on Starlette. | |
| # R-025: X-Forwarded-For MUST NOT be blindly trusted (use rightmost trusted proxy). | |
| # WHY: Attackers spoof X-Forwarded-For to bypass IP rate limits. | |
| # WHERE: app/middleware/rate_limit_middleware.py:76-82 | |
| # STATUS: β οΈ PARTIAL β takes first value, which is user-controllable | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Β§4. INPUT VALIDATION & SANITIZATION | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # R-026: All chat input MUST be sanitized (control chars, zero-width, delimiters). | |
| # WHERE: app/services/guardrails.py β sanitize_input() | |
| # STATUS: β Implemented | |
| # R-027: Chat input hard cap: 2000 characters. No exceptions. | |
| # WHERE: app/services/guardrails.py | |
| # STATUS: β Implemented | |
| # R-028: Jailbreak detection: 30+ regex patterns, β₯2 signals = auto-block. | |
| # WHERE: app/services/guardrails.py β check_boundary() | |
| # STATUS: β Implemented | |
| # R-029: ALL admin API endpoints MUST use Pydantic BaseModel schemas (NOT raw dict). | |
| # WHY: Raw dicts skip validation, type coercion, and OpenAPI documentation. | |
| # WHERE: 16+ endpoints across admin/router.py, chat.py, superadmin/router.py | |
| # STATUS: β NOT IMPLEMENTED β most admin endpoints use body: dict | |
| # AFFECTED: password change, widget config, profile, personality, notifications, | |
| # smart links, Q&A, annotate, flag, track-click, feedback, rate, announce, grant | |
| # R-030: All path parameter IDs MUST be validated as UUID format. | |
| # WHY: Garbage IDs cause unhandled DB errors instead of clean 400s. | |
| # WHERE: All routers β book_id, session_id, message_id, qa_id, grant_id, author_id | |
| # STATUS: β NOT IMPLEMENTED β all IDs are plain str | |
| # R-031: URL inputs (smart links) MUST validate scheme (https preferred) and format. | |
| # WHY: Invalid URLs break widget buttons; internal URLs enable SSRF. | |
| # WHERE: app/admin/router.py:730-733 | |
| # STATUS: β NOT IMPLEMENTED β only length check, no format/scheme validation | |
| # R-032: CSV import MUST sanitize HTML/script content in Q&A answers. | |
| # WHY: Q&A answers are rendered in the widget β XSS vector. | |
| # WHERE: app/admin/router.py:1022-1082 | |
| # STATUS: β οΈ PARTIAL β length-limited but no HTML stripping | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Β§5. FILE UPLOAD SECURITY | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # R-033: MIME type MUST be verified by magic bytes, NEVER by file extension alone. | |
| # WHERE: app/services/file_validator.py via file_utils.validate_upload | |
| # STATUS: β Implemented | |
| # R-034: Cover images MUST strip EXIF metadata (privacy: GPS coords, camera serial). | |
| # WHERE: app/admin/router.py:302 β img.convert('RGB') strips EXIF | |
| # STATUS: β Implemented | |
| # R-035: Upload paths MUST use server-generated names (UUID), NEVER user filenames. | |
| # WHY: User filenames enable directory traversal (../../etc/passwd). | |
| # WHERE: app/api/ingest.py:60 uses book.id (UUID) | |
| # STATUS: β Implemented | |
| # R-036: Temp files MUST be cleaned up on both success AND failure paths. | |
| # WHERE: app/api/ingest.py:175-181 (finally block) | |
| # STATUS: β Implemented | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Β§6. DATA ISOLATION & TENANT SECURITY | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # R-037: Every DB query MUST be scoped by author_id. No cross-tenant data access. | |
| # WHERE: All repositories | |
| # STATUS: β Implemented | |
| # R-038: Admin slug in URL MUST match authenticated user's ID. | |
| # WHERE: app/dependencies.py:168-175 β get_current_author_scoped() | |
| # STATUS: β Implemented | |
| # R-039: ChromaDB collections MUST be namespaced by author_id prefix. | |
| # WHERE: app/services/vector_store.py:133-148 | |
| # STATUS: β Implemented | |
| # R-040: Message annotation/flag endpoints MUST verify session ownership chain. | |
| # WHY: Without it, any author can annotate any other author's messages (IDOR). | |
| # WHERE: app/admin/router.py:858-928 | |
| # STATUS: β NOT IMPLEMENTED β queries by message_id+session_id but NEVER checks | |
| # that ChatSession.author_id == current_user.id | |
| # FIX: Add JOIN to ChatSession and filter by author_id. | |
| # R-041: Visitor fingerprints MUST be one-way hashed (SHA-256), never raw PII. | |
| # WHERE: app/api/chat.py:34-41 | |
| # STATUS: β Implemented | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Β§7. API SECURITY & ERROR HANDLING | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # R-042: Security headers MUST be on every HTTP response. | |
| # Required: X-Content-Type-Options, X-Frame-Options, X-XSS-Protection, | |
| # Referrer-Policy, Permissions-Policy, HSTS (production only). | |
| # WHERE: app/middleware/security_headers.py | |
| # STATUS: β Implemented | |
| # R-043: Content-Security-Policy header MUST be set. | |
| # WHY: CSP is the primary defense against XSS in modern browsers. | |
| # WHERE: app/middleware/security_headers.py | |
| # STATUS: β NOT IMPLEMENTED | |
| # R-044: Error responses MUST NOT contain internal exception messages, SQL, or paths. | |
| # WHY: Information disclosure helps attackers map the system. | |
| # WHERE: app/superadmin/router.py:44-51 β _err() includes str(e)[:200] | |
| # STATUS: β LEAKS DETAILS β returns exception text to client | |
| # R-045: No public/unauthenticated diagnostic endpoints on admin routers. | |
| # WHERE: app/superadmin/router.py:76-79 β GET /api/super/diag has NO auth | |
| # STATUS: β PUBLIC ENDPOINT on SuperAdmin router | |
| # R-046: No duplicate route definitions (causes unpredictable routing). | |
| # WHERE: app/api/chat.py β rate_session defined TWICE (L219 and L346) | |
| # STATUS: β DUPLICATE β first definition silently ignored | |
| # R-047: CORS MUST NOT use wildcard (*) origins in production. | |
| # WHERE: app/main.py β CORSMiddleware config | |
| # STATUS: β οΈ NEEDS VERIFICATION | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Β§8. DATABASE RULES | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # R-048: All schema changes MUST use Alembic migrations. No manual DDL. | |
| # WHY: Manual schema changes risk data loss and cannot be rolled back. | |
| # WHERE: Project root β NO alembic.ini or alembic/ directory exists | |
| # STATUS: β NOT CONFIGURED | |
| # R-049: All queries MUST use parameterized statements (ORM or bound params). | |
| # WHERE: All repositories use SQLAlchemy ORM | |
| # STATUS: β Implemented | |
| # R-050: DB transactions MUST auto-rollback on exception. | |
| # WHERE: app/dependencies.py:57-65 | |
| # STATUS: β Implemented | |
| # R-051: All datetimes MUST use timezone-aware UTC (datetime.now(timezone.utc)). | |
| # WHY: datetime.utcnow() is deprecated and returns naive datetimes. | |
| # WHERE: Mixed β some files use now(timezone.utc), others use utcnow() | |
| # STATUS: β INCONSISTENT β 3+ files still use deprecated utcnow() | |
| # R-052: Concurrent writes MUST be atomic (SQL UPDATE, not Python read-modify-write). | |
| # WHY: ORM-level addition loses increments under concurrent requests. | |
| # WHERE: app/services/token_budget.py:89 | |
| # STATUS: β NOT ATOMIC | |
| # R-053: High-traffic columns MUST have database indexes. | |
| # Required indexes: author_id, session_id, created_at, timestamp | |
| # WHERE: app/models/ β no explicit index definitions visible | |
| # STATUS: β NOT IMPLEMENTED | |
| # R-054: Soft delete SHOULD be preferred for auditable records. | |
| # WHERE: Books and authors use hard delete; only ClientAccess uses soft delete | |
| # STATUS: β οΈ PARTIAL | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Β§9. REDIS INFRASTRUCTURE | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # R-055: Redis MUST have authentication (--requirepass) in production. | |
| # WHERE: Dockerfile β embedded Redis starts without password | |
| # STATUS: β NOT CONFIGURED | |
| # R-056: All Redis keys MUST have TTL β no orphan keys. | |
| # WHERE: app/services/token_budget.py:82, rate_limit_middleware.py:54 | |
| # STATUS: β Implemented | |
| # R-057: Redis connection MUST be pooled. | |
| # WHERE: app/dependencies.py:94-104 | |
| # STATUS: β Implemented | |
| # R-058: Redis failure MUST NOT crash the application (graceful degradation). | |
| # WHERE: All Redis operations wrapped in try/except | |
| # STATUS: β Implemented | |
| # R-059: Redis persistence MUST be explicitly configured (AOF preferred for durability). | |
| # WHERE: Dockerfile | |
| # STATUS: β οΈ Uses default RDB β should configure AOF for session/token data | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Β§10. RAG PIPELINE INTEGRITY | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # R-060: Every chat message MUST pass through ALL 12 pipeline steps. No bypass. | |
| # WHERE: app/services/rag_pipeline.py | |
| # STATUS: β Sequential execution | |
| # R-061: Guardrails MUST run BEFORE any LLM call (input sanitize + boundary check). | |
| # WHERE: Pipeline steps 0-1 run before step 2 | |
| # STATUS: β Implemented | |
| # R-062: Output safety MUST run on EVERY generated response (no AI identity leaks). | |
| # WHERE: app/services/guardrails.py β is_response_safe() | |
| # STATUS: β Implemented | |
| # R-063: Faithfulness NLI check MUST trigger one regeneration, then safe fallback. | |
| # WHERE: app/services/rag_pipeline.py | |
| # STATUS: β Implemented | |
| # R-064: Context window MUST NOT exceed RAG_MAX_CONTEXT_TOKENS (4096). | |
| # WHERE: app/services/context_builder.py | |
| # STATUS: β Hard limit enforced | |
| # R-065: response_style MUST be injected into the LLM system prompt. | |
| # WHY: Admin personality settings currently have ZERO effect on output. | |
| # WHERE: app/services/rag_pipeline.py + app/services/prompter.py | |
| # STATUS: β NOT IMPLEMENTED β style is saved to DB but never reaches the prompt | |
| # R-066: All prompts MUST live in app/services/prompter.py. No inline prompts. | |
| # WHERE: All pipeline services | |
| # STATUS: β Implemented | |
| # R-067: LLM errors MUST return warm human fallback, never a crash or error code. | |
| # WHERE: app/services/rag_pipeline.py β try/except at every step | |
| # STATUS: β Implemented | |
| # R-068: Responses MUST NOT contain markdown (bold, bullets, headers, code blocks). | |
| # WHERE: System prompt + output guardrails | |
| # STATUS: β Enforced | |
| # R-069: No spoilers β never summarize full plot, retell story, or reveal endings. | |
| # WHERE: System prompt instructions | |
| # STATUS: β Enforced in prompt | |
| # R-070: Max response length: ~75 words, 2 paragraphs, 380 chars. | |
| # WHERE: System prompt + formatter | |
| # STATUS: β Enforced | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Β§11. ADMIN PANEL CONSISTENCY | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # R-071: Token usage display MUST include bonus_tokens in total budget. | |
| # WHERE: app/admin/router.py:753-765 via usage_summary_or_empty() | |
| # STATUS: β Implemented | |
| # R-072: Embed token card MUST show same remaining count as token usage page. | |
| # WHERE: Both use tokens_remaining() from token_budget.py | |
| # STATUS: β Implemented | |
| # R-073: Book deletion MUST clean up ChromaDB collection. | |
| # WHERE: app/admin/router.py:243-244 | |
| # STATUS: β Implemented | |
| # R-074: Smart Links MUST sync both books.buy_url AND links.purchase_url. | |
| # WHERE: app/admin/router.py:735-747 | |
| # STATUS: β Implemented | |
| # R-075: Export endpoints MUST have row limits (prevent OOM on large datasets). | |
| # WHERE: app/admin/router.py:1219 β .limit(50000) | |
| # STATUS: β Implemented | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Β§12. SUPERADMIN PANEL | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # R-076: Every SuperAdmin mutation MUST write to the audit log. No exceptions. | |
| # WHERE: app/services/superadmin_service.py β all 7 mutations have _audit.log() | |
| # STATUS: β Implemented | |
| # R-077: Audit log MUST be append-only β no UPDATE or DELETE endpoints. | |
| # WHERE: app/superadmin/router.py:316-344 β only GET | |
| # STATUS: β Implemented | |
| # R-078: Author deletion MUST revoke all active tokens (Redis + DB). | |
| # WHERE: app/services/superadmin_service.py:155-164 | |
| # STATUS: β Implemented | |
| # R-079: Self-deletion and SuperAdmin deletion MUST be prevented. | |
| # WHERE: app/services/superadmin_service.py:148-151 | |
| # STATUS: β Implemented | |
| # R-080: Backup MUST NOT run synchronously on the API event loop. | |
| # WHY: Blocks all concurrent requests during backup. | |
| # WHERE: app/superadmin/router.py:436-444 β run_backup() is synchronous | |
| # STATUS: β RUNS SYNCHRONOUSLY | |
| # FIX: Dispatch to Celery task or use asyncio.to_thread(). | |
| # R-081: Grant-by-email MUST validate plan name against known list. | |
| # WHERE: app/superadmin/router.py:408-413 | |
| # STATUS: β οΈ PARTIAL β plan_map exists but no error for unknown plans | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Β§13. BACKGROUND TASKS & CELERY | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # R-082: Document ingestion MUST use Celery (not asyncio.create_task on API process). | |
| # WHY: asyncio.create_task = lost on restart, no retry, blocks API workers. | |
| # WHERE: app/api/ingest.py:76-85 | |
| # STATUS: β NOT IMPLEMENTED β uses asyncio.create_task() despite Celery task existing | |
| # FIX: Replace with process_document.delay(doc_id, author_id, book_id, path, ext) | |
| # R-083: Celery tasks MUST use acks_late + max_retries for crash resilience. | |
| # WHERE: app/tasks/celery_app.py:35, app/tasks/ingestion_task.py:22-28 | |
| # STATUS: β Configured | |
| # R-084: SSE status broadcasts MUST use consistent Redis channel naming. | |
| # WHERE: ingestion_task.py uses "ingestion:{author_id}" | |
| # but ingest.py uses "ingestion:{author_id}:{book_id}" | |
| # STATUS: β INCONSISTENT β different channel patterns between Celery and asyncio paths | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Β§14. CI/CD & TESTING | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # R-085: CI MUST fail on test failures. `|| true` is FORBIDDEN. | |
| # WHY: Broken code deploys to production automatically. | |
| # WHERE: .github/workflows/ci.yml:57 | |
| # STATUS: β USES `|| true` β tests NEVER block deployment | |
| # R-086: Test coverage MUST meet minimum threshold (β₯80%). | |
| # WHERE: ci.yml:57 β --cov-fail-under is NOT set | |
| # STATUS: β NOT ENFORCED | |
| # R-087: CI MUST include Redis and PostgreSQL services for integration tests. | |
| # WHERE: ci.yml:51-55 β references redis://localhost:6379 but no Redis service | |
| # STATUS: β REDIS NOT AVAILABLE IN CI | |
| # R-088: BDD specs MUST run in CI (behave tests/bdd/features/). | |
| # WHERE: ci.yml β no behave step | |
| # STATUS: β NOT CONFIGURED | |
| # R-089: Deployment MUST be gated on test success (needs: test meaningful only if tests run). | |
| # WHERE: ci.yml:69-70 β needs: test is meaningless with || true | |
| # STATUS: β DEPLOY IS UNGATED | |
| # R-090: Every new function β at least 1 unit test. | |
| # R-091: Every new API endpoint β at least 1 integration test. | |
| # R-092: Every new feature β BDD scenario in .agent/BDD_SPECS.md FIRST. | |
| # R-093: Mock ALL external services in unit tests (OpenAI, DB, Redis, email). | |
| # R-094: Use pytest fixtures for shared setup β no repeated setup code. | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Β§15. OBSERVABILITY & MONITORING | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # R-095: Structured logging (structlog) MUST be used everywhere. No print(). | |
| # WHERE: All services | |
| # STATUS: β Implemented | |
| # R-096: Request/response logging middleware MUST be active. | |
| # WHERE: app/middleware/logging_middleware.py | |
| # STATUS: β Implemented | |
| # R-097: Token values, passwords, API keys MUST never appear in logs. | |
| # WHERE: app/core/access/token_crypto.py:5-6 (rule documented) | |
| # STATUS: β Implemented | |
| # R-098: Health endpoint MUST check DB + Redis + ChromaDB. | |
| # WHERE: app/dependencies.py:216-249 | |
| # STATUS: β Implemented | |
| # R-099: Prometheus /metrics endpoint MUST be configured. | |
| # STATUS: β NOT IMPLEMENTED | |
| # R-100: Error alerting (Sentry or equivalent) MUST be configured. | |
| # STATUS: β NOT IMPLEMENTED | |
| # R-101: Audit log MUST capture actor IP address (for forensics). | |
| # STATUS: β NOT IMPLEMENTED β only captures actor_email | |
| # R-102: Per-step pipeline latency MUST be tracked (not just total response_ms). | |
| # STATUS: β NOT IMPLEMENTED | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Β§16. DEPLOYMENT & INFRASTRUCTURE | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # R-103: Secrets MUST NOT be in version control. | |
| # WHERE: .env in .gitignore | |
| # STATUS: β Implemented | |
| # R-104: Docker image MUST use multi-stage build (minimal production image). | |
| # WHERE: Dockerfile | |
| # STATUS: β Implemented | |
| # R-105: Container SHOULD run as non-root user. | |
| # WHERE: Dockerfile | |
| # STATUS: β RUNS AS ROOT | |
| # R-106: Database backups MUST be automated (Celery Beat schedule). | |
| # WHERE: No backup task in Celery Beat schedule | |
| # STATUS: β NOT SCHEDULED | |
| # R-107: Health check MUST have startup grace period. | |
| # WHERE: Dockerfile β 180s start period | |
| # STATUS: β Implemented | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Β§17. CODE STRUCTURE CONVENTIONS | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # R-108: All configuration from app/config.py (pydantic BaseSettings). Never hardcode. | |
| # R-109: All DI via FastAPI Depends() in app/dependencies.py. | |
| # R-110: All DB access via repository pattern (app/repositories/). | |
| # R-111: Business logic in app/services/ β never in route handlers. | |
| # R-112: Background work in Celery tasks (app/tasks/) β never asyncio.create_task for long work. | |
| # R-113: Custom exceptions in app/exceptions/ β never raise generic Exception. | |
| # R-114: Every module MUST have a module-level docstring. | |
| # R-115: Every function MUST have type-annotated signature + docstring. | |
| # R-116: No function longer than 40 lines β refactor into named helpers. | |
| # R-117: Update FILE_MAP.md when adding or removing any file. | |
| # R-118: Update BDD_SPECS.md before implementing any new user-facing feature. | |
| # R-119: Read .agent/RULES.md and .agent/FILE_MAP.md before every coding session. | |
| # R-120: All destructive UI actions require confirmation dialog with typed confirmation. | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Β§18. FRONTEND & WIDGET SECURITY | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # R-121: Widget escHtml() MUST escape ALL 5 HTML entities (&, <, >, ", '). | |
| # WHY: Incomplete escaping enables XSS in bot responses rendered as innerHTML. | |
| # WHERE: static/widget.js:483-484 | |
| # STATUS: β Implemented β all 5 entities covered | |
| # R-122: Widget link buttons MUST always use target="_blank" rel="noopener". | |
| # WHY: Without noopener, linked pages can hijack the parent window (reverse tabnabbing). | |
| # WHERE: static/widget.js:371 | |
| # STATUS: β Implemented | |
| # R-123: Widget MUST NOT expose subscription token in DOM or global JS scope. | |
| # WHY: Any page script can steal the token from window variables or DOM attributes. | |
| # WHERE: static/widget.js:22-38 β token stored in closure variable (not window) | |
| # STATUS: β Token stays in IIFE closure | |
| # R-124: Admin SPA MUST redirect to /login on 401 response (not show broken UI). | |
| # WHY: Stale JWTs leave admin in a broken state with no clear recovery. | |
| # WHERE: static/auth-client.js | |
| # STATUS: β οΈ PARTIAL β auth-client.js exists but redirect behavior varies by page | |
| # R-125: /widget endpoint MUST NOT serve with Access-Control-Allow-Origin: *. | |
| # WHY: Exposes widget test page to cross-origin embedding by any domain. | |
| # WHERE: app/main.py:294 β explicitly adds "*" CORS header on /widget response | |
| # STATUS: β WILDCARD CORS β anyone can embed the test page | |
| # R-126: /covers static mount MUST disable directory listing. | |
| # WHY: Attackers can enumerate all cover images and map author IDs from filenames. | |
| # WHERE: app/main.py:265-266 β StaticFiles(directory=covers_dir) | |
| # STATUS: β οΈ StaticFiles does NOT list by default, but doesn't 404 on /covers/ root | |
| # R-127: Widget size budget: widget.js MUST stay under 25KB minified. | |
| # WHY: Widget loads on third-party author websites; large JS hurts page speed. | |
| # WHERE: static/widget.js β currently 19.7KB unminified (523 lines) | |
| # STATUS: β οΈ Under budget unminified, but no minification or CDN configured | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Β§19. CELERY TASKS β DATA QUALITY | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # R-128: Weekly digest email MUST populate actual stats (not hardcoded 0). | |
| # WHY: Emails with "0 sessions, 0% tokens" destroy author trust. | |
| # WHERE: app/tasks/email_task.py:39-46 β chat_count=0, token_pct=0, top_country="β" | |
| # STATUS: β PLACEHOLDER DATA β all values hardcoded to zero | |
| # R-129: Token warning email MUST include actual tokens_used and tokens_total. | |
| # WHY: "Your usage is at 80%" with tokens_used=0 is confusing and useless. | |
| # WHERE: app/tasks/email_task.py:70-76 β tokens_used=0, tokens_total=0 | |
| # STATUS: β PLACEHOLDER DATA | |
| # R-130: Link health check MUST NOT follow redirects to internal/private IPs (SSRF). | |
| # WHY: An author could set purchase_url to http://169.254.169.254/metadata (AWS IMDS). | |
| # WHERE: app/tasks/link_health_task.py:34 β follow_redirects=True with NO IP blocklist | |
| # STATUS: β NOT PROTECTED β follows redirects blindly, including to internal IPs | |
| # R-131: Celery Beat MUST include the backup task in its schedule. | |
| # WHY: backup_task.py exists but is NOT registered in beat_schedule. | |
| # WHERE: app/tasks/celery_app.py:40-66 | |
| # STATUS: β MISSING β backup task exists (backup_task.py) but never scheduled | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Β§20. STARTUP & INITIALIZATION | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # R-132: Schema migrations MUST NOT use raw ALTER TABLE in application startup code. | |
| # WHY: Raw f-string ALTER TABLE is fragile, non-reversible, and can silently fail. | |
| # Also, f"ALTER TABLE {table} ADD COLUMN {col}" has SQL injection risk if table/col | |
| # values ever come from untrusted input (currently hardcoded, but bad pattern). | |
| # WHERE: app/main.py:78-101 β manual ALTER TABLE loop in _init_db() | |
| # STATUS: β Uses raw DDL instead of Alembic (blocked until R-048 is done) | |
| # R-133: OpenAPI docs (/docs, /redoc) MUST be disabled or auth-gated in production. | |
| # WHY: Public OpenAPI documentation maps every endpoint, parameter, and schema | |
| # for an attacker. This is the #1 reconnaissance tool. | |
| # WHERE: app/main.py:155-156 β docs_url="/docs", redoc_url="/redoc" always enabled | |
| # STATUS: β PUBLIC β anyone can browse full API docs | |
| # R-134: SuperAdmin seed MUST generate TOTP secret on first creation. | |
| # WHY: If R-007 (TOTP enforcement) is implemented but no totp_secret exists | |
| # on the seeded SuperAdmin, they'll be permanently locked out. | |
| # WHERE: app/main.py:131-139 β creates User without totp_secret | |
| # STATUS: β NOT IMPLEMENTED β User model has no totp_secret column either | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Β§21. DEPLOYMENT β DOCKER & SERVICES | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # R-135: docker-compose.yml MUST NOT use deprecated 'version' key. | |
| # WHY: Docker Compose v2+ ignores the version key; keeping it causes warnings | |
| # and confuses developers about which spec is being used. | |
| # WHERE: docker-compose.yml:1 β version: "3.9" | |
| # STATUS: β οΈ Works but deprecated β remove the line | |
| # R-136: Flower (Celery monitoring) MUST require authentication. | |
| # WHY: Flower exposes task names, arguments, worker status, and broker URLs. | |
| # WHERE: docker-compose.yml:123 β celery flower started with no auth flags | |
| # STATUS: β NO AUTH β add --basic-auth=user:password or --auth=email | |
| # R-137: docker-compose api command MUST NOT use --reload in production. | |
| # WHY: --reload enables code injection via filesystem writes and wastes CPU. | |
| # WHERE: docker-compose.yml:79 β uvicorn ... --reload | |
| # STATUS: β οΈ PARTIAL β file is marked "Development" but no production override exists | |
| # R-138: User cascade delete MUST clean up ChromaDB collections. | |
| # WHERE: app/services/superadmin_service.py | |
| # STATUS: β Implemented (author_prefix collection purge) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Β§22. ENTERPRISE WORTH ROADMAP (R-139βR-173) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # R-139: Weekly digest MUST use real analytics β STATUS: β email_task + analytics_repo.weekly_digest_stats | |
| # R-140: Token budget warning email at 80% β STATUS: β token_budget + EmailService.send_token_budget_warning | |
| # R-141: Admin analytics UI API-backed only β STATUS: β admin.html loadAnalytics | |
| # R-142: Link-click funnel counts conversions β STATUS: β track_link_click + funnel_counts | |
| # R-143: JWT refresh blacklists refresh jti β STATUS: β schemas_router refresh | |
| # R-144: Logout blacklists refresh jti β STATUS: β schemas_router logout | |
| # R-145: SuperAdmin mandatory TOTP β STATUS: β dependencies.get_current_superadmin | |
| # R-146: Q&A CSV HTML strip β STATUS: β qa_service._sanitize_qa_text | |
| # R-147β149: Book lifecycle β STATUS: β books router + book_service | |
| # R-150: Celery ingestion β STATUS: β ingestion_service.schedule_file_ingestion | |
| # R-151: Duplicate upload warning β STATUS: β start_file_upload duplicate check | |
| # R-152: Cross-book RAG β STATUS: β pipeline core + ask_all_books | |
| # R-157: Alembic migrations β STATUS: β alembic/ + db.py no ALTER loop | |
| # R-158: Celery deploy in compose β STATUS: β docker-compose context fixed | |
| # R-159: prometheus_client + sentry-sdk β STATUS: β requirements.txt | |
| # R-161: Author delete Chroma purge β STATUS: β superadmin_service | |
| # R-165: BDD feature files β STATUS: β tests/bdd/features/enterprise_roadmap.feature | |
| # R-166: CI mypy β STATUS: β ci.yml | |
| # R-167: CI Postgres+Redis β STATUS: β ci.yml services | |
| # R-169: Onboarding checklist β STATUS: β admin/routers/onboarding.py | |
| # R-172: Competitor guard β STATUS: β guardrails.check_boundary | |
| # R-173: Billing webhooks β STATUS: β billing_service + billing_router | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # SUMMARY: 173 RULES (R-001βR-173) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # β Implemented: 70 rules | |
| # β Not Implemented: 47 rules (must fix) | |
| # β οΈ Partial: 21 rules (needs improvement) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |