databricks-tellr-app 0.3.13.dev1__tar.gz → 0.3.13.dev2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/PKG-INFO +1 -1
  2. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/databricks_tellr_app.egg-info/PKG-INFO +1 -1
  3. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/databricks_tellr_app.egg-info/SOURCES.txt +4 -0
  4. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/pyproject.toml +1 -1
  5. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/main.py +18 -0
  6. databricks_tellr_app-0.3.13.dev2/src/api/middleware/csrf.py +109 -0
  7. databricks_tellr_app-0.3.13.dev2/src/api/middleware/security_headers.py +97 -0
  8. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/services/chat_service.py +9 -70
  9. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/README.md +0 -0
  10. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/databricks_tellr_app/__init__.py +0 -0
  11. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/databricks_tellr_app/run.py +0 -0
  12. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/databricks_tellr_app.egg-info/dependency_links.txt +0 -0
  13. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/databricks_tellr_app.egg-info/requires.txt +0 -0
  14. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/databricks_tellr_app.egg-info/top_level.txt +0 -0
  15. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/setup.cfg +0 -0
  16. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/setup.py +0 -0
  17. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/__init__.py +0 -0
  18. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/__init__.py +0 -0
  19. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/_otel_bootstrap.py +0 -0
  20. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/fixtures/tour_demo_deck.json +0 -0
  21. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/mcp_auth.py +0 -0
  22. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/mcp_server.py +0 -0
  23. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/middleware/__init__.py +0 -0
  24. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/middleware/request_logging.py +0 -0
  25. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/routes/__init__.py +0 -0
  26. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/routes/admin.py +0 -0
  27. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/routes/admin_usage.py +0 -0
  28. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/routes/agent_config.py +0 -0
  29. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/routes/chat.py +0 -0
  30. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/routes/deck_contributors.py +0 -0
  31. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/routes/export.py +0 -0
  32. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/routes/feedback.py +0 -0
  33. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/routes/google_slides.py +0 -0
  34. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/routes/images.py +0 -0
  35. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/routes/local_version.py +0 -0
  36. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/routes/profiles.py +0 -0
  37. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/routes/sessions.py +0 -0
  38. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/routes/settings/__init__.py +0 -0
  39. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/routes/settings/contributors.py +0 -0
  40. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/routes/settings/deck_prompts.py +0 -0
  41. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/routes/settings/identities.py +0 -0
  42. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/routes/settings/slide_styles.py +0 -0
  43. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/routes/setup.py +0 -0
  44. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/routes/slides.py +0 -0
  45. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/routes/tools.py +0 -0
  46. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/routes/tour.py +0 -0
  47. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/routes/verification.py +0 -0
  48. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/routes/version.py +0 -0
  49. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/schemas/__init__.py +0 -0
  50. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/schemas/agent_config.py +0 -0
  51. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/schemas/feedback.py +0 -0
  52. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/schemas/requests.py +0 -0
  53. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/schemas/responses.py +0 -0
  54. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/schemas/settings/__init__.py +0 -0
  55. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/schemas/settings/requests.py +0 -0
  56. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/schemas/settings/responses.py +0 -0
  57. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/schemas/streaming.py +0 -0
  58. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/services/__init__.py +0 -0
  59. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/services/export_job_queue.py +0 -0
  60. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/services/feedback_service.py +0 -0
  61. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/services/job_queue.py +0 -0
  62. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/services/session_manager.py +0 -0
  63. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/services/session_naming.py +0 -0
  64. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/services/usage_events.py +0 -0
  65. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/services/usage_service.py +0 -0
  66. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/utils/__init__.py +0 -0
  67. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/api/utils/validation.py +0 -0
  68. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/core/__init__.py +0 -0
  69. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/core/config_loader.py +0 -0
  70. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/core/context_utils.py +0 -0
  71. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/core/database.py +0 -0
  72. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/core/databricks_client.py +0 -0
  73. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/core/defaults.py +0 -0
  74. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/core/encryption.py +0 -0
  75. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/core/init_default_profile.py +0 -0
  76. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/core/lakebase.py +0 -0
  77. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/core/migrate_profiles_to_agent_config.py +0 -0
  78. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/core/mlflow_agent_spans.py +0 -0
  79. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/core/mlflow_tracing.py +0 -0
  80. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/core/otel_logging.py +0 -0
  81. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/core/permission_context.py +0 -0
  82. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/core/prompt_modules.py +0 -0
  83. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/core/settings_db.py +0 -0
  84. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/core/user_context.py +0 -0
  85. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/database/__init__.py +0 -0
  86. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/database/models/__init__.py +0 -0
  87. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/database/models/deck_contributor.py +0 -0
  88. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/database/models/feedback.py +0 -0
  89. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/database/models/genie_space.py +0 -0
  90. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/database/models/google_global_credentials.py +0 -0
  91. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/database/models/google_oauth_token.py +0 -0
  92. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/database/models/identity.py +0 -0
  93. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/database/models/image.py +0 -0
  94. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/database/models/profile.py +0 -0
  95. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/database/models/profile_contributor.py +0 -0
  96. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/database/models/prompts.py +0 -0
  97. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/database/models/request_log.py +0 -0
  98. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/database/models/session.py +0 -0
  99. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/database/models/slide_deck_prompt.py +0 -0
  100. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/database/models/slide_style_library.py +0 -0
  101. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/database/models/usage_event.py +0 -0
  102. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/database/models/user_preference.py +0 -0
  103. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/domain/__init__.py +0 -0
  104. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/domain/slide.py +0 -0
  105. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/domain/slide_deck.py +0 -0
  106. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/services/__init__.py +0 -0
  107. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/services/agent.py +0 -0
  108. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/services/agent_factory.py +0 -0
  109. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/services/config_service.py +0 -0
  110. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/services/config_validator.py +0 -0
  111. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/services/drive_uploader.py +0 -0
  112. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/services/evaluation/__init__.py +0 -0
  113. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/services/evaluation/llm_judge.py +0 -0
  114. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/services/genie_service.py +0 -0
  115. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/services/google_slides_auth.py +0 -0
  116. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/services/google_slides_prompts_defaults.py +0 -0
  117. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/services/html_to_google_slides.py +0 -0
  118. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/services/html_to_pptx.py +0 -0
  119. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/services/identity_provider.py +0 -0
  120. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/services/identity_providers/__init__.py +0 -0
  121. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/services/identity_providers/account_provider.py +0 -0
  122. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/services/identity_providers/local_provider.py +0 -0
  123. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/services/identity_providers/workspace_provider.py +0 -0
  124. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/services/image_service.py +0 -0
  125. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/services/image_tools.py +0 -0
  126. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/services/permission_service.py +0 -0
  127. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/services/pptx_from_html_huashu.py +0 -0
  128. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/services/pptx_from_records.py +0 -0
  129. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/services/pptx_prompts_defaults.py +0 -0
  130. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/services/profile_service.py +0 -0
  131. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/services/streaming_callback.py +0 -0
  132. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/services/tools/__init__.py +0 -0
  133. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/services/tools/agent_bricks_tool.py +0 -0
  134. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/services/tools/genie_tool.py +0 -0
  135. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/services/tools/mcp_tool.py +0 -0
  136. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/services/tools/model_endpoint_tool.py +0 -0
  137. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/services/tools/vector_tool.py +0 -0
  138. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/services/validator.py +0 -0
  139. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/utils/__init__.py +0 -0
  140. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/utils/css_utils.py +0 -0
  141. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/utils/error_handling.py +0 -0
  142. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/utils/html_safety.py +0 -0
  143. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/utils/html_utils.py +0 -0
  144. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/utils/image_utils.py +0 -0
  145. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/utils/js_validator.py +0 -0
  146. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/utils/logging_config.py +0 -0
  147. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/utils/pi_filter.py +0 -0
  148. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/utils/slide_hash.py +0 -0
  149. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/utils/spotlight.py +0 -0
  150. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev2}/src/utils/text_caps.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: databricks-tellr-app
3
- Version: 0.3.13.dev1
3
+ Version: 0.3.13.dev2
4
4
  Summary: Tellr application package for Databricks Apps
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: databricks-tellr-app
3
- Version: 0.3.13.dev1
3
+ Version: 0.3.13.dev2
4
4
  Summary: Tellr application package for Databricks Apps
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -11,7 +11,9 @@ setup.py
11
11
  ./src/api/mcp_server.py
12
12
  ./src/api/fixtures/tour_demo_deck.json
13
13
  ./src/api/middleware/__init__.py
14
+ ./src/api/middleware/csrf.py
14
15
  ./src/api/middleware/request_logging.py
16
+ ./src/api/middleware/security_headers.py
15
17
  ./src/api/routes/__init__.py
16
18
  ./src/api/routes/admin.py
17
19
  ./src/api/routes/admin_usage.py
@@ -154,7 +156,9 @@ src/api/mcp_auth.py
154
156
  src/api/mcp_server.py
155
157
  src/api/fixtures/tour_demo_deck.json
156
158
  src/api/middleware/__init__.py
159
+ src/api/middleware/csrf.py
157
160
  src/api/middleware/request_logging.py
161
+ src/api/middleware/security_headers.py
158
162
  src/api/routes/__init__.py
159
163
  src/api/routes/admin.py
160
164
  src/api/routes/admin_usage.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "databricks-tellr-app"
7
- version = "0.3.13.dev1"
7
+ version = "0.3.13.dev2"
8
8
  description = "Tellr application package for Databricks Apps"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -417,6 +417,24 @@ from src.api.middleware.request_logging import RequestLoggingMiddleware
417
417
 
418
418
  app.add_middleware(RequestLoggingMiddleware)
419
419
 
420
+ # === SDR-4437 Track A security middleware (PR-1a/PR-1b) ==================
421
+ # Keep ALL security-middleware registration inside this delimited block
422
+ # (PR-2 adds an exception handler below the middleware block — do not edit
423
+ # outside these markers). Starlette wraps later-added middleware outermost;
424
+ # ordering here is deliberate:
425
+ # SecurityHeaders (outermost, added last) -> CSRF -> RequestLogging ->
426
+ # user_auth -> CORS(dev) -> normalize_mcp_path -> routes
427
+ # Headers must stamp EVERY response (including CSRF 403s); CSRF must reject
428
+ # forged cross-site requests before OBO auth work and before request_logs
429
+ # DB writes. CSRF runs outside normalize_mcp_path, so it sees the raw
430
+ # un-rewritten /mcp path (its exemption handles both /mcp and /mcp/).
431
+ from src.api.middleware.csrf import CSRFProtectionMiddleware # noqa: E402
432
+ from src.api.middleware.security_headers import SecurityHeadersMiddleware # noqa: E402
433
+
434
+ app.add_middleware(CSRFProtectionMiddleware)
435
+ app.add_middleware(SecurityHeadersMiddleware)
436
+ # === end SDR-4437 Track A security middleware =============================
437
+
420
438
  # Include API routers
421
439
  app.include_router(admin.router)
422
440
  app.include_router(admin_usage.router)
@@ -0,0 +1,109 @@
1
+ """Origin-validation CSRF protection (SDR-4437 CRITICAL-2, PR-1b).
2
+
3
+ Threat model: the app sets no cookies of its own — auth arrives via
4
+ proxy-attested ``x-forwarded-*`` headers. The CSRF-relevant ambient
5
+ credential is the platform SSO cookie at the Databricks Apps proxy: a
6
+ cross-site browser request to the app URL rides that cookie, the proxy
7
+ authenticates it and forwards it carrying the victim's identity.
8
+ Validating the browser-attested ``Origin`` (falling back to ``Referer``)
9
+ against the app's own origin closes this at the app layer.
10
+
11
+ Deliberate choices (documented for security review):
12
+
13
+ - **Neither header present -> allow.** Cross-origin browser POSTs always
14
+ send ``Origin``, so the realistic attack always presents a mismatch;
15
+ header-less mutating requests are non-browser clients (scripts/curl via
16
+ the proxy), which rejecting would break for no security gain. A strict
17
+ mode can be revisited if security review requires it.
18
+ - **/mcp is exempt.** It authenticates with bearer tokens, not cookies, so
19
+ it is not a CSRF target. The mounted sub-app IS wrapped by the parent
20
+ app's middleware (that is how ``normalize_mcp_path`` works), so the
21
+ exemption must be this explicit path check, not an assumption that
22
+ mounts bypass middleware. This middleware runs OUTSIDE
23
+ ``normalize_mcp_path``, so it sees the raw ``/mcp`` (un-rewritten) path
24
+ — both ``/mcp`` and ``/mcp/...`` are matched; ``/mcp-anything`` is not.
25
+ - **Expected origin:** ``DATABRICKS_APP_URL`` (platform-injected on
26
+ Databricks Apps deploys — same assumption as
27
+ ``src/api/mcp_server.py::_public_app_url``). If unset, fall back to the
28
+ origin reconstructed from ``x-forwarded-proto``/``x-forwarded-host``
29
+ (the proxy always sends these; same pattern as
30
+ ``google_slides.py::_build_redirect_uri``), so a missing var degrades to
31
+ a self-referential check instead of blocking all writes.
32
+ - **Inactive outside production** — consistent with the dev-only CORS gate
33
+ in ``main.py`` (dev auth is ``DEV_USER_ID`` with no proxy in front).
34
+ """
35
+
36
+ import logging
37
+ import os
38
+ from urllib.parse import urlsplit
39
+
40
+ from starlette.middleware.base import BaseHTTPMiddleware
41
+ from starlette.requests import Request
42
+ from starlette.responses import JSONResponse, Response
43
+
44
+ logger = logging.getLogger(__name__)
45
+
46
+ _UNSAFE_METHODS = frozenset({"POST", "PUT", "PATCH", "DELETE"})
47
+
48
+
49
+ def _origin_of(url: str) -> str:
50
+ """Normalize a URL (or Origin header value) to ``scheme://host[:port]``.
51
+
52
+ Returns "" for values with no parseable scheme+host (e.g. ``null``).
53
+ """
54
+ parts = urlsplit(url.strip())
55
+ if not parts.scheme or not parts.netloc:
56
+ return ""
57
+ return f"{parts.scheme.lower()}://{parts.netloc.lower()}"
58
+
59
+
60
+ class CSRFProtectionMiddleware(BaseHTTPMiddleware):
61
+ """Reject state-changing requests whose Origin/Referer is cross-site."""
62
+
63
+ def __init__(self, app, enabled: bool | None = None):
64
+ super().__init__(app)
65
+ self.enabled = (
66
+ enabled
67
+ if enabled is not None
68
+ else os.getenv("ENVIRONMENT", "development") == "production"
69
+ )
70
+
71
+ async def dispatch(self, request: Request, call_next) -> Response:
72
+ if not self.enabled or request.method not in _UNSAFE_METHODS:
73
+ return await call_next(request)
74
+
75
+ path = request.url.path
76
+ if path == "/mcp" or path.startswith("/mcp/"):
77
+ return await call_next(request)
78
+
79
+ claimed = request.headers.get("origin") or request.headers.get("referer")
80
+ if not claimed:
81
+ return await call_next(request)
82
+
83
+ claimed_origin = _origin_of(claimed)
84
+ expected_origin = self._expected_origin(request)
85
+ if claimed_origin and expected_origin and claimed_origin == expected_origin:
86
+ return await call_next(request)
87
+
88
+ logger.warning(
89
+ "CSRF: rejected cross-origin %s %s (claimed=%r, expected=%r)",
90
+ request.method,
91
+ path,
92
+ claimed_origin,
93
+ expected_origin,
94
+ )
95
+ return JSONResponse(
96
+ status_code=403,
97
+ content={"detail": "Cross-origin request rejected"},
98
+ )
99
+
100
+ @staticmethod
101
+ def _expected_origin(request: Request) -> str:
102
+ app_url = os.getenv("DATABRICKS_APP_URL", "")
103
+ if app_url:
104
+ return _origin_of(app_url)
105
+ host = request.headers.get("x-forwarded-host", "")
106
+ proto = request.headers.get("x-forwarded-proto", "https")
107
+ if host:
108
+ return f"{proto.strip().lower()}://{host.split(',')[0].strip().lower()}"
109
+ return ""
@@ -0,0 +1,97 @@
1
+ """App-origin security headers (SDR-4437 CRITICAL-1, PR-1a).
2
+
3
+ Sets browser hardening headers on every response that flows back through
4
+ the middleware stack (known limit: raw 500s from genuinely unhandled
5
+ exceptions are synthesized by Starlette's ``ServerErrorMiddleware``
6
+ outside all user middleware and are NOT stamped; those bodies are
7
+ ``text/plain`` and non-executable), plus a
8
+ Content-Security-Policy differentiated by response type:
9
+
10
+ - HTML documents (the SPA index/catch-all AND the Google OAuth callback
11
+ popup, which carries inline <script>) get ``DOCUMENT_CSP``. Two
12
+ constraints rule out a strict policy here:
13
+
14
+ 1. srcdoc inheritance: slides render in ``srcdoc`` iframes, and srcdoc
15
+ documents inherit the embedding document's CSP in addition to their
16
+ own SLIDE_CSP meta tag (both policies enforce; most restrictive
17
+ wins). A ``default-src 'self'`` header on the SPA would block every
18
+ slide's inline <script>, Chart.js/Tailwind CDN loads, and Google
19
+ Fonts — breaking slide rendering app-wide. The document policy must
20
+ therefore be a superset of SLIDE_CSP's allowances
21
+ (``src/utils/html_safety.py``; sync-tested in
22
+ ``tests/unit/test_export_csp.py``).
23
+ 2. Inline styles: the React app uses ``style={{...}}`` attributes and
24
+ Radix UI injects inline styles at runtime; CSP hashes apply to
25
+ <style>/<script> *elements*, not style *attributes*, so
26
+ ``style-src`` needs 'unsafe-inline'.
27
+
28
+ ``script-src 'unsafe-inline'`` + CDNs is a deliberate, documented
29
+ trade-off forced by srcdoc inheritance: slides remain confined by the
30
+ stricter SLIDE_CSP meta policy (connect-src 'none', form-action
31
+ 'none', no eval) plus the html_safety.py scanner, and any header at
32
+ all is a strict improvement over the previous no-CSP state.
33
+
34
+ - Everything else (JSON APIs, assets, SSE) gets the deny-all
35
+ ``API_CSP`` — nothing executes in a JSON response.
36
+
37
+ ``frame-ancestors 'none'`` / ``X-Frame-Options: DENY`` is safe: all
38
+ in-app slide iframes use ``srcdoc`` (never a backend-served ``src``), and
39
+ the only other HTML endpoint, the Google OAuth callback, opens as a
40
+ popup, not an iframe.
41
+
42
+ ``frame-src 'self'`` does not affect the srcdoc slide iframes: a srcdoc
43
+ document loads no URL (its address is the local scheme ``about:srcdoc``,
44
+ no fetch occurs), and per CSP3 local-scheme handling plus current
45
+ Chrome/Firefox/Safari behavior it is NOT subject to frame-src source
46
+ matching — even ``frame-src 'none'`` pages can create srcdoc iframes.
47
+ Instead it inherits this document policy, which is exactly the
48
+ srcdoc-inheritance constraint handled above. Pre-decided fallback if a
49
+ browser ever refuses the slide iframe with a frame-src violation
50
+ ("Refused to frame ..."): ``frame-src 'self' about:`` — the ``about:``
51
+ scheme-source matches ``about:srcdoc`` (the historical workaround for
52
+ old-Chrome behavior that did URL-match about: frames).
53
+ """
54
+
55
+ from starlette.middleware.base import BaseHTTPMiddleware
56
+ from starlette.requests import Request
57
+ from starlette.responses import Response
58
+
59
+ # CSP for HTML document responses: union of SPA needs and SLIDE_CSP
60
+ # allowances (kept a superset of SLIDE_CSP by test_export_csp.py).
61
+ DOCUMENT_CSP = (
62
+ "default-src 'self'; "
63
+ "script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdn.tailwindcss.com; "
64
+ "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; "
65
+ "img-src 'self' data:; "
66
+ "font-src 'self' data: https://cdn.jsdelivr.net https://fonts.gstatic.com; "
67
+ "connect-src 'self'; "
68
+ "frame-src 'self'; "
69
+ "frame-ancestors 'none'; "
70
+ "object-src 'none'; "
71
+ "base-uri 'none'"
72
+ )
73
+
74
+ # CSP for everything else.
75
+ API_CSP = "default-src 'none'; frame-ancestors 'none'"
76
+
77
+ _STATIC_HEADERS = {
78
+ "X-Frame-Options": "DENY",
79
+ "X-Content-Type-Options": "nosniff",
80
+ "Referrer-Policy": "same-origin",
81
+ "Strict-Transport-Security": "max-age=31536000; includeSubDomains",
82
+ }
83
+
84
+
85
+ class SecurityHeadersMiddleware(BaseHTTPMiddleware):
86
+ """Stamp security headers on every response (registered outermost)."""
87
+
88
+ async def dispatch(self, request: Request, call_next) -> Response:
89
+ response = await call_next(request)
90
+ for name, value in _STATIC_HEADERS.items():
91
+ response.headers[name] = value
92
+ content_type = response.headers.get("content-type", "")
93
+ if content_type.startswith("text/html"):
94
+ response.headers["Content-Security-Policy"] = DOCUMENT_CSP
95
+ else:
96
+ response.headers["Content-Security-Policy"] = API_CSP
97
+ return response
@@ -80,11 +80,6 @@ class ChatService:
80
80
  # This avoids re-parsing HTML on every request
81
81
  self._deck_cache: Dict[str, SlideDeck] = {}
82
82
 
83
- # DB version each cached deck corresponds to. The cache is per-process
84
- # while prod runs multiple uvicorn workers sharing one database, so a
85
- # cache hit is only valid if the DB version hasn't moved on.
86
- self._deck_cache_versions: Dict[str, int] = {}
87
-
88
83
  logger.info("ChatService initialized successfully")
89
84
 
90
85
  def _substitute_images_for_response(self, deck_dict, raw_html=None):
@@ -605,7 +600,7 @@ class ChatService:
605
600
  slide_deck_dict = current_deck.to_dict()
606
601
 
607
602
  try:
608
- save_result = session_manager.save_slide_deck(
603
+ session_manager.save_slide_deck(
609
604
  session_id=session_id,
610
605
  title=current_deck.title,
611
606
  html_content=current_deck.knit(),
@@ -615,7 +610,6 @@ class ChatService:
615
610
  modified_by=_user,
616
611
  expected_version=_deck_version_before_llm,
617
612
  )
618
- self._record_deck_version(session_id, save_result)
619
613
  except VersionConflictError:
620
614
  logger.warning(
621
615
  "Chat save rejected: deck was edited during LLM call, reloading",
@@ -1391,7 +1385,7 @@ class ChatService:
1391
1385
  slide_deck_dict = current_deck.to_dict()
1392
1386
 
1393
1387
  try:
1394
- save_result = session_manager.save_slide_deck(
1388
+ session_manager.save_slide_deck(
1395
1389
  session_id=session_id,
1396
1390
  title=current_deck.title,
1397
1391
  html_content=current_deck.knit(),
@@ -1401,7 +1395,6 @@ class ChatService:
1401
1395
  modified_by=_user,
1402
1396
  expected_version=_deck_version_before_llm,
1403
1397
  )
1404
- self._record_deck_version(session_id, save_result)
1405
1398
  except VersionConflictError:
1406
1399
  logger.warning(
1407
1400
  "Chat save rejected: deck was edited during LLM call, reloading",
@@ -1928,63 +1921,18 @@ class ChatService:
1928
1921
  context = "\n\n[Attached images]\n" + "\n".join(image_descriptions)
1929
1922
  return message + context
1930
1923
 
1931
- def _deck_versions(self) -> Dict[str, int]:
1932
- """Return the cached-deck version map, creating it if needed.
1933
-
1934
- Lazy creation keeps instances built without __init__ (test harnesses
1935
- use ChatService.__new__) working. Callers must hold _cache_lock.
1936
- """
1937
- try:
1938
- return self._deck_cache_versions
1939
- except AttributeError:
1940
- self._deck_cache_versions = {}
1941
- return self._deck_cache_versions
1942
-
1943
- def _record_deck_version(self, session_id: str, save_result: Optional[Dict[str, Any]]) -> None:
1944
- """Remember which DB version the cached deck now corresponds to.
1945
-
1946
- Call after save_slide_deck so the next _get_or_load_deck cache hit
1947
- validates cleanly instead of reloading from the database.
1948
- """
1949
- version = (save_result or {}).get("version")
1950
- with self._cache_lock:
1951
- if version is not None:
1952
- self._deck_versions()[session_id] = version
1953
- else:
1954
- self._deck_versions().pop(session_id, None)
1955
-
1956
1924
  def _get_or_load_deck(self, session_id: str) -> Optional[SlideDeck]:
1957
1925
  """Get deck from cache or load from database.
1958
1926
 
1959
1927
  Thread-safe access to deck cache using _cache_lock.
1960
-
1961
- The cache is per-process while prod runs multiple uvicorn workers
1962
- sharing one database, so a cached deck is only served if its recorded
1963
- version matches the current DB version; otherwise it is reloaded.
1964
-
1928
+
1965
1929
  Uses deck_dict (slides array with individual scripts) when available,
1966
1930
  falling back to from_html_string for legacy data.
1967
1931
  """
1968
1932
  # Check cache first (with lock)
1969
1933
  with self._cache_lock:
1970
- cached_deck = self._deck_cache.get(session_id)
1971
- cached_version = self._deck_versions().get(session_id)
1972
-
1973
- if cached_deck is not None:
1974
- db_version = self._get_deck_version(session_id)
1975
- if db_version is None or cached_version == db_version:
1976
- return cached_deck
1977
- logger.info(
1978
- "Deck cache stale, reloading from database",
1979
- extra={
1980
- "session_id": session_id,
1981
- "cached_version": cached_version,
1982
- "db_version": db_version,
1983
- },
1984
- )
1985
- with self._cache_lock:
1986
- self._deck_cache.pop(session_id, None)
1987
- self._deck_versions().pop(session_id, None)
1934
+ if session_id in self._deck_cache:
1935
+ return self._deck_cache[session_id]
1988
1936
 
1989
1937
  # Try to load from database (outside lock to avoid blocking)
1990
1938
  session_manager = get_session_manager()
@@ -2018,10 +1966,6 @@ class ChatService:
2018
1966
  # Store in cache (with lock)
2019
1967
  with self._cache_lock:
2020
1968
  self._deck_cache[session_id] = deck
2021
- if deck_data.get("version") is not None:
2022
- self._deck_versions()[session_id] = deck_data["version"]
2023
- else:
2024
- self._deck_versions().pop(session_id, None)
2025
1969
  return deck
2026
1970
  except Exception as e:
2027
1971
  logger.warning(f"Failed to load deck from database: {e}")
@@ -2064,7 +2008,6 @@ class ChatService:
2064
2008
  """Remove the cached deck for a session so the next read hits the DB."""
2065
2009
  with self._cache_lock:
2066
2010
  self._deck_cache.pop(session_id, None)
2067
- self._deck_versions().pop(session_id, None)
2068
2011
 
2069
2012
  def _replace_slide_htmls_from_cache(self, session_id: str, slide_context: Dict[str, Any]) -> Dict[str, Any]:
2070
2013
  """Replace frontend-supplied slide_htmls with backend cache versions.
@@ -2597,7 +2540,7 @@ class ChatService:
2597
2540
  # Persist to database
2598
2541
  deck_dict = current_deck.to_dict()
2599
2542
  session_manager = get_session_manager()
2600
- save_result = session_manager.save_slide_deck(
2543
+ session_manager.save_slide_deck(
2601
2544
  session_id=session_id,
2602
2545
  title=current_deck.title,
2603
2546
  html_content=current_deck.knit(),
@@ -2606,7 +2549,6 @@ class ChatService:
2606
2549
  deck_dict=deck_dict,
2607
2550
  expected_version=expected_version,
2608
2551
  )
2609
- self._record_deck_version(session_id, save_result)
2610
2552
 
2611
2553
  # Create save point
2612
2554
  try:
@@ -2674,7 +2616,7 @@ class ChatService:
2674
2616
  # Persist to database
2675
2617
  deck_dict = current_deck.to_dict()
2676
2618
  session_manager = get_session_manager()
2677
- save_result = session_manager.save_slide_deck(
2619
+ session_manager.save_slide_deck(
2678
2620
  session_id=session_id,
2679
2621
  title=current_deck.title,
2680
2622
  html_content=current_deck.knit(),
@@ -2683,7 +2625,6 @@ class ChatService:
2683
2625
  deck_dict=deck_dict,
2684
2626
  expected_version=expected_version,
2685
2627
  )
2686
- self._record_deck_version(session_id, save_result)
2687
2628
 
2688
2629
  # Create save point immediately after persisting
2689
2630
  try:
@@ -2740,7 +2681,7 @@ class ChatService:
2740
2681
  # Persist to database
2741
2682
  deck_dict = current_deck.to_dict()
2742
2683
  session_manager = get_session_manager()
2743
- save_result = session_manager.save_slide_deck(
2684
+ session_manager.save_slide_deck(
2744
2685
  session_id=session_id,
2745
2686
  title=current_deck.title,
2746
2687
  html_content=current_deck.knit(),
@@ -2749,7 +2690,6 @@ class ChatService:
2749
2690
  deck_dict=deck_dict,
2750
2691
  expected_version=expected_version,
2751
2692
  )
2752
- self._record_deck_version(session_id, save_result)
2753
2693
 
2754
2694
  # Create save point
2755
2695
  try:
@@ -2804,7 +2744,7 @@ class ChatService:
2804
2744
  # Persist to database
2805
2745
  deck_dict = current_deck.to_dict()
2806
2746
  session_manager = get_session_manager()
2807
- save_result = session_manager.save_slide_deck(
2747
+ session_manager.save_slide_deck(
2808
2748
  session_id=session_id,
2809
2749
  title=current_deck.title,
2810
2750
  html_content=current_deck.knit(),
@@ -2813,7 +2753,6 @@ class ChatService:
2813
2753
  deck_dict=deck_dict,
2814
2754
  expected_version=expected_version,
2815
2755
  )
2816
- self._record_deck_version(session_id, save_result)
2817
2756
 
2818
2757
  # Create save point
2819
2758
  try: