Arag / .cursorrules
AuthorBot
Implement Enterprise Worth Roadmap (R-139–R-173).
df480e4
Raw
History Blame Contribute Delete
42.4 kB
# ═══════════════════════════════════════════════════════════════════════════════
# 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)
# ═══════════════════════════════════════════════════════════════════════════════