iatoolkit 2.46.0__tar.gz → 2.47.0__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 (254) hide show
  1. {iatoolkit-2.46.0/src/iatoolkit.egg-info → iatoolkit-2.47.0}/PKG-INFO +2 -1
  2. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/requirements.txt +1 -0
  3. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/__init__.py +1 -1
  4. iatoolkit-2.47.0/src/iatoolkit/common/url_safety.py +163 -0
  5. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/common/util.py +12 -3
  6. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/connectors/local_file_connector.py +27 -3
  7. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/llm_providers/openai_adapter.py +6 -2
  8. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/llm_client_service.py +14 -4
  9. iatoolkit-2.47.0/src/iatoolkit/services/operator_identity_service.py +136 -0
  10. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/storage_service.py +27 -0
  11. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/static/js/chat_main.js +62 -2
  12. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/prompt_api_view.py +26 -6
  13. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/tool_api_view.py +29 -9
  14. {iatoolkit-2.46.0 → iatoolkit-2.47.0/src/iatoolkit.egg-info}/PKG-INFO +2 -1
  15. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit.egg-info/SOURCES.txt +1 -0
  16. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit.egg-info/requires.txt +1 -0
  17. iatoolkit-2.46.0/src/iatoolkit/services/operator_identity_service.py +0 -114
  18. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/LICENSE +0 -0
  19. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/LICENSE_COMMUNITY.md +0 -0
  20. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/pyproject.toml +0 -0
  21. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/readme.md +0 -0
  22. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/setup.cfg +0 -0
  23. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/base_company.py +0 -0
  24. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/cli_commands.py +0 -0
  25. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/common/__init__.py +0 -0
  26. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/common/bridge_validation.py +0 -0
  27. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/common/exceptions.py +0 -0
  28. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/common/interfaces/__init__.py +0 -0
  29. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/common/interfaces/asset_storage.py +0 -0
  30. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/common/interfaces/database_provider.py +0 -0
  31. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/common/interfaces/memory_compilation_trigger.py +0 -0
  32. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/common/interfaces/memory_lint_trigger.py +0 -0
  33. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/common/interfaces/secret_provider.py +0 -0
  34. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/common/interfaces/signup_policy_resolver.py +0 -0
  35. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/common/interfaces/web_search_provider.py +0 -0
  36. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/common/model_registry.py +0 -0
  37. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/common/routes.py +0 -0
  38. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/common/secret_resolver.py +0 -0
  39. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/common/session_manager.py +0 -0
  40. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/company_registry.py +0 -0
  41. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/__init__.py +0 -0
  42. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/llm_capabilities.yaml +0 -0
  43. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/channel_business_identity.prompt +0 -0
  44. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/channel_conversation_continuity.prompt +0 -0
  45. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/channel_response_style.prompt +0 -0
  46. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/chat_state_rules.prompt +0 -0
  47. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/chat_user_profile.prompt +0 -0
  48. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/core_identity.prompt +0 -0
  49. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/email_output.prompt +0 -0
  50. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/file_download_output.prompt +0 -0
  51. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/format_styles.prompt +0 -0
  52. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/html_structures.prompt +0 -0
  53. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/links_documents.prompt +0 -0
  54. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/memory_usage.prompt +0 -0
  55. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/multimodal_basics.prompt +0 -0
  56. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/output_basics.prompt +0 -0
  57. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/presentation_formatting.prompt +0 -0
  58. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/query_main.prompt +0 -0
  59. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/sql_aggregation_scope.prompt +0 -0
  60. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/sql_casting.prompt +0 -0
  61. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/sql_core.prompt +0 -0
  62. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/sql_jsonb.prompt +0 -0
  63. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/sql_mysql_casting.prompt +0 -0
  64. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/sql_mysql_json.prompt +0 -0
  65. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/sql_redshift_basics.prompt +0 -0
  66. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/sql_redshift_types.prompt +0 -0
  67. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/sql_rules.prompt +0 -0
  68. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/tool_html_passthrough.prompt +0 -0
  69. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts_pack.yaml +0 -0
  70. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_tools_pack.yaml +0 -0
  71. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/core.py +0 -0
  72. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/__init__.py +0 -0
  73. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/brevo_mail_app.py +0 -0
  74. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/call_service.py +0 -0
  75. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/connectors/__init__.py +0 -0
  76. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/connectors/file_connector.py +0 -0
  77. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/connectors/file_connector_factory.py +0 -0
  78. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/connectors/google_cloud_storage_connector.py +0 -0
  79. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/connectors/google_drive_connector.py +0 -0
  80. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/connectors/s3_connector.py +0 -0
  81. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/google_auth_client.py +0 -0
  82. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/google_chat_app.py +0 -0
  83. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/inference_embeddings_client.py +0 -0
  84. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/inference_handler.py +0 -0
  85. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/jina_embeddings_client.py +0 -0
  86. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/llm_gateway_resolver.py +0 -0
  87. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/llm_providers/__init__.py +0 -0
  88. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/llm_providers/anthropic_adapter.py +0 -0
  89. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/llm_providers/deepseek_adapter.py +0 -0
  90. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/llm_providers/gemini_adapter.py +0 -0
  91. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/llm_providers/openai_compatible_chat_adapter.py +0 -0
  92. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/llm_providers/openrouter_adapter.py +0 -0
  93. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/llm_proxy.py +0 -0
  94. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/llm_response.py +0 -0
  95. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/redis_session_manager.py +0 -0
  96. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/locales/en.yaml +0 -0
  97. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/locales/es.yaml +0 -0
  98. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/repositories/__init__.py +0 -0
  99. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/repositories/api_key_repo.py +0 -0
  100. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/repositories/bridge_repo.py +0 -0
  101. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/repositories/database_manager.py +0 -0
  102. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/repositories/document_repo.py +0 -0
  103. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/repositories/env_secret_provider.py +0 -0
  104. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/repositories/filesystem_asset_repository.py +0 -0
  105. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/repositories/knowledge_wiki_repo.py +0 -0
  106. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/repositories/llm_query_repo.py +0 -0
  107. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/repositories/mcp_token_repo.py +0 -0
  108. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/repositories/memory_repo.py +0 -0
  109. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/repositories/models.py +0 -0
  110. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/repositories/profile_repo.py +0 -0
  111. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/repositories/prompt_resource_repo.py +0 -0
  112. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/repositories/sql_dataset_repo.py +0 -0
  113. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/repositories/sql_source_repo.py +0 -0
  114. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/repositories/vs_repo.py +0 -0
  115. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/runtime_logging.py +0 -0
  116. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/__init__.py +0 -0
  117. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/api_key_service.py +0 -0
  118. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/attachment_policy_service.py +0 -0
  119. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/auth_service.py +0 -0
  120. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/benchmark_service.py +0 -0
  121. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/branding_service.py +0 -0
  122. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/company_context_service.py +0 -0
  123. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/configuration_service.py +0 -0
  124. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/context_builder_service.py +0 -0
  125. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/dispatcher_service.py +0 -0
  126. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/embedding_service.py +0 -0
  127. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/excel_service.py +0 -0
  128. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/file_processor_service.py +0 -0
  129. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/history_manager_service.py +0 -0
  130. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/http_tool_service.py +0 -0
  131. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/i18n_service.py +0 -0
  132. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/inference_service.py +0 -0
  133. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/jwt_service.py +0 -0
  134. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/knowledge_base_service.py +0 -0
  135. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/language_service.py +0 -0
  136. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/license_service.py +0 -0
  137. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/mail_service.py +0 -0
  138. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/markdown_wiki_service.py +0 -0
  139. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/mcp_token_service.py +0 -0
  140. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/memory_compiler_service.py +0 -0
  141. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/memory_lint_service.py +0 -0
  142. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/memory_lookup_policy_service.py +0 -0
  143. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/memory_service.py +0 -0
  144. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/memory_wiki_service.py +0 -0
  145. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/noop_memory_compilation_trigger.py +0 -0
  146. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/noop_memory_lint_trigger.py +0 -0
  147. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/parsers/__init__.py +0 -0
  148. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/parsers/contracts.py +0 -0
  149. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/parsers/image_normalizer.py +0 -0
  150. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/parsers/parsing_service.py +0 -0
  151. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/parsers/pdf_ocr_detection.py +0 -0
  152. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/parsers/provider_factory.py +0 -0
  153. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/parsers/provider_resolver.py +0 -0
  154. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/parsers/providers/__init__.py +0 -0
  155. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/parsers/providers/basic_provider.py +0 -0
  156. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/parsers/providers/docling_provider.py +0 -0
  157. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/parsers/validator.py +0 -0
  158. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/pdf_service.py +0 -0
  159. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/profile_service.py +0 -0
  160. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/prompt_resource_service.py +0 -0
  161. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/prompt_service.py +0 -0
  162. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/query_service.py +0 -0
  163. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/signup_policy_resolver.py +0 -0
  164. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/sql_dataset_service.py +0 -0
  165. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/sql_service.py +0 -0
  166. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/sql_source_service.py +0 -0
  167. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/structured_output_service.py +0 -0
  168. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/system_prompt_catalog.py +0 -0
  169. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/system_tools.py +0 -0
  170. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/telemetry_service.py +0 -0
  171. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/tenant_wiki_service.py +0 -0
  172. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/tool_output_contract.py +0 -0
  173. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/tool_service.py +0 -0
  174. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/user_feedback_service.py +0 -0
  175. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/user_session_context_service.py +0 -0
  176. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/visual_kb_service.py +0 -0
  177. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/visual_tool_service.py +0 -0
  178. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/warmup_service.py +0 -0
  179. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/web_search/__init__.py +0 -0
  180. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/web_search/provider_factory.py +0 -0
  181. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/web_search/providers/__init__.py +0 -0
  182. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/web_search/providers/brave_provider.py +0 -0
  183. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/web_search_service.py +0 -0
  184. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/static/images/fernando.jpeg +0 -0
  185. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/static/images/iatoolkit_core.png +0 -0
  186. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/static/images/iatoolkit_logo.png +0 -0
  187. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/static/js/chat_feedback_button.js +0 -0
  188. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/static/js/chat_filepond.js +0 -0
  189. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/static/js/chat_help_content.js +0 -0
  190. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/static/js/chat_history_button.js +0 -0
  191. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/static/js/chat_logout_button.js +0 -0
  192. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/static/js/chat_memory_button.js +0 -0
  193. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/static/js/chat_model_selector.js +0 -0
  194. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/static/js/chat_onboarding_button.js +0 -0
  195. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/static/js/chat_prompt_manager.js +0 -0
  196. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/static/js/chat_reload_button.js +0 -0
  197. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/static/styles/chat_iatoolkit.css +0 -0
  198. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/static/styles/chat_modal.css +0 -0
  199. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/static/styles/chat_public.css +0 -0
  200. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/static/styles/documents.css +0 -0
  201. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/static/styles/landing_page.css +0 -0
  202. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/static/styles/llm_output.css +0 -0
  203. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/static/styles/onboarding.css +0 -0
  204. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/templates/_company_header.html +0 -0
  205. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/templates/_login_widget.html +0 -0
  206. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/templates/account.html +0 -0
  207. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/templates/base.html +0 -0
  208. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/templates/change_password.html +0 -0
  209. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/templates/chat.html +0 -0
  210. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/templates/chat_memory_modal.html +0 -0
  211. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/templates/chat_modals.html +0 -0
  212. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/templates/error.html +0 -0
  213. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/templates/forgot_password.html +0 -0
  214. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/templates/home_hosted_default.html +0 -0
  215. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/templates/memory/wiki_schema.md +0 -0
  216. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/templates/onboarding_shell.html +0 -0
  217. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/templates/pdf/base.html +0 -0
  218. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/templates/pdf/letter.html +0 -0
  219. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/templates/pdf/report.html +0 -0
  220. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/templates/pdf/simple.html +0 -0
  221. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/templates/signup.html +0 -0
  222. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/__init__.py +0 -0
  223. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/account_view.py +0 -0
  224. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/api_key_api_view.py +0 -0
  225. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/base_login_view.py +0 -0
  226. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/categories_api_view.py +0 -0
  227. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/change_password_view.py +0 -0
  228. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/chat_context_preview_api_view.py +0 -0
  229. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/chat_view.py +0 -0
  230. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/configuration_api_view.py +0 -0
  231. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/connectors_api_view.py +0 -0
  232. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/embedding_api_view.py +0 -0
  233. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/forgot_password_view.py +0 -0
  234. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/help_content_api_view.py +0 -0
  235. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/history_api_view.py +0 -0
  236. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/home_view.py +0 -0
  237. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/init_context_api_view.py +0 -0
  238. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/llmquery_api_view.py +0 -0
  239. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/load_document_api_view.py +0 -0
  240. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/login_view.py +0 -0
  241. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/logout_api_view.py +0 -0
  242. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/memory_api_view.py +0 -0
  243. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/profile_api_view.py +0 -0
  244. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/prompt_context_preview_api_view.py +0 -0
  245. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/prompt_resource_api_view.py +0 -0
  246. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/rag_api_view.py +0 -0
  247. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/root_redirect_view.py +0 -0
  248. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/signup_view.py +0 -0
  249. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/static_page_view.py +0 -0
  250. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/user_feedback_api_view.py +0 -0
  251. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/users_api_view.py +0 -0
  252. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/verify_user_view.py +0 -0
  253. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit.egg-info/dependency_links.txt +0 -0
  254. {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iatoolkit
3
- Version: 2.46.0
3
+ Version: 2.47.0
4
4
  Summary: IAToolkit
5
5
  Author: Fernando Libedinsky
6
6
  License-Expression: MIT
@@ -51,6 +51,7 @@ Requires-Dist: langchain==0.3.19
51
51
  Requires-Dist: langchain-core==0.3.35
52
52
  Requires-Dist: langchain-text-splitters==0.3.6
53
53
  Requires-Dist: markdown2==2.5.3
54
+ Requires-Dist: nh3==0.3.7
54
55
  Requires-Dist: openai==2.8.1
55
56
  Requires-Dist: openpyxl==3.1.5
56
57
  Requires-Dist: pandas==2.3.1
@@ -43,6 +43,7 @@ langchain==0.3.19
43
43
  langchain-core==0.3.35
44
44
  langchain-text-splitters==0.3.6
45
45
  markdown2==2.5.3
46
+ nh3==0.3.7
46
47
  openai==2.8.1
47
48
  openpyxl==3.1.5
48
49
  pandas==2.3.1
@@ -3,7 +3,7 @@
3
3
  #
4
4
  # IAToolkit is open source software.
5
5
 
6
- __version__ = "2.46.0"
6
+ __version__ = "2.47.0"
7
7
 
8
8
  # Expose main classes and functions at the top level of the package
9
9
 
@@ -0,0 +1,163 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
+
6
+ """
7
+ Shared SSRF guard for URLs that arrive from tenants or API callers and that the
8
+ server will then connect to itself: task callback URLs, attachment downloads,
9
+ MCP image fetches, and similar.
10
+
11
+ Checking only literal IPs (``https://10.0.0.5/...``) is not enough - an
12
+ attacker-controlled hostname can resolve to 10.x / 169.254.169.254 / ::1 just
13
+ as well, and a public URL can 302 to one. This module therefore:
14
+
15
+ * requires an absolute URL with an allowed scheme and a hostname;
16
+ * rejects well-known local names (localhost, *.local, *.localhost, *.internal);
17
+ * rejects literal IPs that are private, loopback, link-local, multicast,
18
+ reserved or unspecified;
19
+ * resolves hostnames and rejects them when ANY resolved address falls in those
20
+ ranges (fail-closed on a private answer; fail-open only when the name cannot
21
+ be resolved at all, since that request could not have reached anything);
22
+ * offers ``fetch_with_safe_redirects`` which never lets ``requests`` follow a
23
+ redirect blindly: every hop is re-validated with the same rules.
24
+
25
+ Validate-then-connect still leaves a small DNS-rebinding window (the resolver
26
+ may return a different address at connect time); closing it fully needs
27
+ connection-level IP pinning, which is out of scope here. Callers should also
28
+ disable redirects on the actual request or use ``fetch_with_safe_redirects``.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import ipaddress
34
+ import socket
35
+ from urllib.parse import ParseResult, urljoin, urlparse
36
+
37
+ import requests
38
+
39
+ from iatoolkit.common.exceptions import IAToolkitException
40
+
41
+ _BLOCKED_HOSTNAMES = {"localhost", "localhost.localdomain"}
42
+ _BLOCKED_HOST_SUFFIXES = (".local", ".localhost", ".internal")
43
+ _REDIRECT_STATUSES = {301, 302, 303, 307, 308}
44
+
45
+
46
+ def classify_ip(ip_value: ipaddress.IPv4Address | ipaddress.IPv6Address) -> str:
47
+ """'blocked' for loopback/link-local/multicast/reserved/unspecified,
48
+ 'private' for RFC1918 & friends, 'public' otherwise."""
49
+ if (
50
+ ip_value.is_loopback
51
+ or ip_value.is_link_local
52
+ or ip_value.is_multicast
53
+ or ip_value.is_reserved
54
+ or ip_value.is_unspecified
55
+ ):
56
+ return "blocked"
57
+ if ip_value.is_private:
58
+ return "private"
59
+ # IPv4-mapped IPv6 (::ffff:10.0.0.1) must be judged by the embedded IPv4.
60
+ mapped = getattr(ip_value, "ipv4_mapped", None)
61
+ if mapped is not None:
62
+ return classify_ip(mapped)
63
+ return "public"
64
+
65
+
66
+ def _to_ip_or_none(host: str):
67
+ try:
68
+ return ipaddress.ip_address(host)
69
+ except ValueError:
70
+ return None
71
+
72
+
73
+ def resolve_host_classifications(hostname: str) -> list[str] | None:
74
+ """Resolves ``hostname`` and classifies every address. Returns None when the
75
+ name cannot be resolved (caller decides how to treat that)."""
76
+ try:
77
+ entries = socket.getaddrinfo(hostname, None)
78
+ except Exception:
79
+ return None
80
+
81
+ classifications: list[str] = []
82
+ for entry in entries:
83
+ sockaddr = entry[4]
84
+ if not sockaddr:
85
+ continue
86
+ ip_value = _to_ip_or_none(str(sockaddr[0]).split("%", 1)[0])
87
+ if ip_value is not None:
88
+ classifications.append(classify_ip(ip_value))
89
+ return classifications
90
+
91
+
92
+ def assert_public_http_url(
93
+ url: str,
94
+ *,
95
+ allowed_schemes: tuple[str, ...] = ("https",),
96
+ error_type: IAToolkitException.ErrorType = IAToolkitException.ErrorType.INVALID_PARAMETER,
97
+ label: str = "URL",
98
+ ) -> ParseResult:
99
+ """
100
+ Raises IAToolkitException(error_type) unless ``url`` is an absolute URL with
101
+ an allowed scheme whose host is a public internet address (literal or via
102
+ DNS). Returns the parsed URL on success.
103
+ """
104
+ normalized = str(url or "").strip()
105
+ parsed = urlparse(normalized)
106
+ scheme = (parsed.scheme or "").strip().lower()
107
+ hostname = (parsed.hostname or "").strip().lower().rstrip(".")
108
+
109
+ if scheme not in allowed_schemes:
110
+ raise IAToolkitException(
111
+ error_type,
112
+ f"{label} must use {'/'.join(s.upper() for s in allowed_schemes)}.",
113
+ )
114
+ if not hostname:
115
+ raise IAToolkitException(error_type, f"{label} host is required.")
116
+ if hostname in _BLOCKED_HOSTNAMES or hostname.endswith(_BLOCKED_HOST_SUFFIXES):
117
+ raise IAToolkitException(error_type, f"{label} host is not allowed.")
118
+
119
+ ip_value = _to_ip_or_none(hostname)
120
+ if ip_value is not None:
121
+ if classify_ip(ip_value) != "public":
122
+ raise IAToolkitException(error_type, f"{label} host is not allowed.")
123
+ return parsed
124
+
125
+ classifications = resolve_host_classifications(hostname)
126
+ if classifications and any(kind != "public" for kind in classifications):
127
+ raise IAToolkitException(
128
+ error_type,
129
+ f"{label} host is not allowed (resolves to a private or reserved address).",
130
+ )
131
+ return parsed
132
+
133
+
134
+ def fetch_with_safe_redirects(
135
+ url: str,
136
+ *,
137
+ allowed_schemes: tuple[str, ...] = ("https",),
138
+ error_type: IAToolkitException.ErrorType = IAToolkitException.ErrorType.INVALID_PARAMETER,
139
+ label: str = "URL",
140
+ max_redirects: int = 3,
141
+ **requests_kwargs,
142
+ ) -> requests.Response:
143
+ """
144
+ ``requests.get`` that validates the initial URL and every redirect hop with
145
+ ``assert_public_http_url`` instead of letting ``requests`` follow them
146
+ blindly (a public URL that 302s to http://169.254.169.254/ would otherwise
147
+ sail through). Extra kwargs (timeout, stream, headers, ...) are passed to
148
+ ``requests.get``; ``allow_redirects`` is always forced to False.
149
+ """
150
+ requests_kwargs.pop("allow_redirects", None)
151
+ current_url = str(url or "").strip()
152
+ for _ in range(max_redirects + 1):
153
+ assert_public_http_url(
154
+ current_url, allowed_schemes=allowed_schemes, error_type=error_type, label=label
155
+ )
156
+ response = requests.get(current_url, allow_redirects=False, **requests_kwargs)
157
+ location = response.headers.get("Location") if response.status_code in _REDIRECT_STATUSES else None
158
+ if not location:
159
+ return response
160
+ response.close()
161
+ current_url = urljoin(current_url, location)
162
+
163
+ raise IAToolkitException(error_type, f"{label} redirected too many times.")
@@ -9,7 +9,16 @@ from iatoolkit.common.exceptions import IAToolkitException
9
9
  from flask import request
10
10
  from injector import inject
11
11
  import os
12
- from jinja2 import Environment, FileSystemLoader
12
+ from jinja2 import FileSystemLoader
13
+ # Prompt templates are authored by tenant users through the GUI/API, not by the
14
+ # operator running this server, so they must render in a sandbox: a plain
15
+ # jinja2.Environment lets a template walk `cycler.__init__.__globals__.os` (or
16
+ # any object's __class__/__mro__/__subclasses__) straight into the interpreter
17
+ # and execute commands on the host. ImmutableSandboxedEnvironment blocks
18
+ # underscore-prefixed attribute access, unsafe callables, and state-mutating
19
+ # methods while leaving everything a prompt legitimately needs (variables,
20
+ # loops, conditionals, filters, includes) intact.
21
+ from jinja2.sandbox import ImmutableSandboxedEnvironment
13
22
  from datetime import datetime, date
14
23
  from decimal import Decimal
15
24
  import yaml
@@ -33,7 +42,7 @@ class Utility:
33
42
  template_dir = os.path.dirname(template_pathname)
34
43
  template_file = os.path.basename(template_pathname)
35
44
 
36
- env = Environment(loader=FileSystemLoader(template_dir))
45
+ env = ImmutableSandboxedEnvironment(loader=FileSystemLoader(template_dir))
37
46
  template = env.get_template(template_file)
38
47
 
39
48
  # Explicit kwargs should win over client_data to preserve rich objects
@@ -71,7 +80,7 @@ class Utility:
71
80
  else:
72
81
  loader = None # Sin loader, no se pueden incluir plantillas desde archivos.
73
82
 
74
- env = Environment(loader=loader)
83
+ env = ImmutableSandboxedEnvironment(loader=loader)
75
84
  template = env.from_string(template_string)
76
85
 
77
86
  # Explicit kwargs should win over client_data to preserve rich objects
@@ -37,9 +37,33 @@ class LocalFileConnector(FileConnector):
37
37
  raise IAToolkitException(IAToolkitException.ErrorType.FILE_IO_ERROR,
38
38
  f"Error procesando el directorio {self.directory}: {e}")
39
39
 
40
+ def _resolve_within_root(self, file_path: str) -> str:
41
+ """
42
+ Maps a storage key / path onto the filesystem and refuses anything that
43
+ escapes the connector's root directory. `file_path` may be relative (a
44
+ storage key) or absolute (the `path` values list_files() hands back);
45
+ either way the real, symlink-resolved target must stay under root -
46
+ otherwise a caller-controlled key like `../../etc/passwd` or
47
+ `/proc/self/environ` turns this connector into an arbitrary file
48
+ read/write/delete on the host.
49
+ """
50
+ candidate = str(file_path or "").strip()
51
+ if not candidate or "\x00" in candidate:
52
+ raise IAToolkitException(IAToolkitException.ErrorType.INVALID_PARAMETER,
53
+ "Invalid storage path.")
54
+
55
+ root = os.path.realpath(self.directory)
56
+ target = candidate if os.path.isabs(candidate) else os.path.join(root, candidate)
57
+ resolved = os.path.realpath(target)
58
+ if resolved != root and not resolved.startswith(root + os.sep):
59
+ raise IAToolkitException(IAToolkitException.ErrorType.PERMISSION,
60
+ f"Storage path escapes the configured root directory: {file_path}")
61
+ return resolved
62
+
40
63
  def get_file_content(self, file_path: str) -> bytes:
64
+ full_path = self._resolve_within_root(file_path)
41
65
  try:
42
- with open(file_path, 'rb') as f:
66
+ with open(full_path, 'rb') as f:
43
67
  return f.read()
44
68
  except Exception as e:
45
69
  raise IAToolkitException(IAToolkitException.ErrorType.FILE_IO_ERROR,
@@ -47,7 +71,7 @@ class LocalFileConnector(FileConnector):
47
71
 
48
72
  def upload_file(self, file_path: str, content: bytes, content_type: str = None) -> None:
49
73
  # Nota: file_path debe ser relativo al directorio raíz configurado
50
- full_path = os.path.join(self.directory, file_path)
74
+ full_path = self._resolve_within_root(file_path)
51
75
 
52
76
  # Asegurar que el directorio destino existe
53
77
  try:
@@ -59,7 +83,7 @@ class LocalFileConnector(FileConnector):
59
83
  f"Error escribiendo el archivo {file_path}: {e}")
60
84
 
61
85
  def delete_file(self, file_path: str) -> None:
62
- full_path = os.path.join(self.directory, file_path)
86
+ full_path = self._resolve_within_root(file_path)
63
87
  try:
64
88
  if os.path.exists(full_path):
65
89
  os.remove(full_path)
@@ -92,12 +92,16 @@ class OpenAIAdapter:
92
92
  Important cases:
93
93
  - Assistant messages with `tool_calls` must become top-level `function_call` items.
94
94
  - `function_call_output` items must not include internal-only fields such as `status`.
95
+ - Without `previous_response_id`, include client-side `context_history` before
96
+ the current turn so OpenAI can run from locally managed history if needed.
95
97
  - When continuing from a prior response with `previous_response_id`, tool follow-ups
96
98
  should only append the `function_call_output` items because the previous response
97
99
  already contains the originating user/assistant turns on OpenAI's side.
98
100
  """
99
- _ = context_history
100
- combined_input: List[Dict[str, Any]] = list(input_items or [])
101
+ combined_input: List[Dict[str, Any]] = []
102
+ if not previous_response_id:
103
+ combined_input.extend(context_history or [])
104
+ combined_input.extend(input_items or [])
101
105
 
102
106
  has_function_outputs = any(
103
107
  isinstance(item, dict) and item.get("type") == "function_call_output"
@@ -12,6 +12,7 @@ from iatoolkit.common.model_registry import ModelRegistry
12
12
  from injector import inject
13
13
  import time
14
14
  import markdown2
15
+ import nh3
15
16
  import os
16
17
  import logging
17
18
  import json
@@ -1393,10 +1394,19 @@ class llmClient:
1393
1394
 
1394
1395
  # Heurística simple: si contiene tags, lo tratamos como HTML ya renderizable
1395
1396
  if re.search(r"</?[a-zA-Z][\s\S]*>", answer):
1396
- return answer.replace("\n", "")
1397
-
1398
- html_answer = markdown2.markdown(answer).replace("\n", "")
1399
- return html_answer
1397
+ html_answer = answer.replace("\n", "")
1398
+ else:
1399
+ html_answer = markdown2.markdown(answer).replace("\n", "")
1400
+
1401
+ # The result is injected into the chat DOM as HTML (and stored, then shown
1402
+ # again in the admin monitoring inspector). The model echoes whatever it
1403
+ # was fed - SQL rows, RAG documents, tool results - so an <img onerror>,
1404
+ # <svg onload> or javascript: link planted in tenant data would otherwise
1405
+ # run in every viewer's browser, including admins'. nh3 is an allowlist
1406
+ # sanitizer: it keeps the formatting markdown2 produces (headings, lists,
1407
+ # tables, code, links, images) and strips scripts, event handlers,
1408
+ # iframes/objects and javascript:/data: URLs.
1409
+ return nh3.clean(html_answer)
1400
1410
 
1401
1411
  def format_plaintext(self, answer: str):
1402
1412
  if not answer:
@@ -0,0 +1,136 @@
1
+ # Copyright (c) 2024 Fernando Libedinsky
2
+ # Product: IAToolkit
3
+ #
4
+ # IAToolkit is open source software.
5
+
6
+ import logging
7
+ import os
8
+
9
+ from injector import inject, singleton
10
+
11
+ from iatoolkit.common.exceptions import IAToolkitException
12
+
13
+
14
+ @singleton
15
+ class OperatorIdentityService:
16
+ """Who operates this deployment, and who collects from it — two questions.
17
+
18
+ They were one, answered by one name, and that held only because on our own
19
+ deployment the answers coincide. They came apart the first time a customer ran
20
+ IAToolkit on its own infrastructure: that installation *is* operated by its
21
+ company — its accounts live there, its console belongs to it — and it collects
22
+ from nobody. The monthly close refused to run because it asked for a merchant
23
+ account that had no reason to exist.
24
+
25
+ So:
26
+
27
+ * **The operator** is identity. Where this deployment's own accounts live,
28
+ which company you must be signed into to reach the control center, and which
29
+ company the console leaves out of its own portfolio. Every installation has
30
+ one.
31
+ * **The billing company** is money. Whose merchant account charges a card,
32
+ whose bank details sign a transfer notice, whose Stripe configuration holds
33
+ the webhook secret. Only an installation that collects has one, and it
34
+ defaults to the operator because usually they are the same company — but not
35
+ always: the company that runs the deployment and the entity that issues the
36
+ invoices can be in different countries.
37
+
38
+ Lives in the core because the core, Enterprise and the website all ask it, and
39
+ a rule restated in three repositories drifts. Same reason ``ServiceModel`` is
40
+ here.
41
+ """
42
+
43
+ #: Identity. Read on every request that reaches the console.
44
+ SETTING_NAME = "IAT_OPERATOR_COMPANY"
45
+
46
+ #: Money. Optional: absent means "the operator collects", which is the common
47
+ #: case. Named separately so an installation that collects for a different
48
+ #: legal entity can say so, and so one that collects for nobody can leave it
49
+ #: empty instead of inventing a merchant.
50
+ BILLING_SETTING_NAME = "IAT_BILLING_COMPANY_SHORT_NAME"
51
+
52
+ #: Held on the class, not the instance. Callers that have a repository at hand
53
+ #: build their own instance rather than asking the injector, so an instance
54
+ #: cache resolved — and logged, and queried the database — once per request
55
+ #: instead of once per process.
56
+ _resolved: str | None = None
57
+
58
+ @classmethod
59
+ def reset_cache(cls) -> None:
60
+ """Forget the resolved name. For tests: it cannot change at runtime."""
61
+ OperatorIdentityService._resolved = None
62
+
63
+ @classmethod
64
+ def stated_name(cls) -> str:
65
+ """The operator, as this deployment names it. Empty when nothing names it.
66
+
67
+ For callers with no database at hand — and there is nothing to verify: an
68
+ explicitly configured name was never checked against the database, and a
69
+ company that does not exist cannot authenticate anyone.
70
+
71
+ Empty rather than a guess. The name of another deployment travelling into
72
+ this one is what made a monthly close resolve the payment configuration of
73
+ a company that was not there and mark every invoice failed.
74
+ """
75
+ return (os.getenv(cls.SETTING_NAME) or "").strip().lower()
76
+
77
+ @classmethod
78
+ def stated_billing_name(cls) -> str:
79
+ """Who collects, as this deployment names it. Empty when nobody does."""
80
+ explicit = (os.getenv(cls.BILLING_SETTING_NAME) or "").strip().lower()
81
+ return explicit or cls.stated_name()
82
+
83
+ @inject
84
+ def __init__(self):
85
+ # No database: since the legacy fallback went away there is nothing to
86
+ # verify. The answers are two environment variables and a cache.
87
+ pass
88
+
89
+ def company_short_name(self) -> str:
90
+ """The operator company, resolved once per process.
91
+
92
+ Cached because the answer cannot change without a deployment, and this is
93
+ read on paths that run per request.
94
+ """
95
+ if OperatorIdentityService._resolved:
96
+ return OperatorIdentityService._resolved
97
+
98
+ configured = self.stated_name()
99
+ if configured:
100
+ OperatorIdentityService._resolved = configured
101
+ logging.info("Operator company: '%s' (from %s).", configured, self.SETTING_NAME)
102
+ return configured
103
+
104
+ # Deliberately loud, and about identity rather than money: without this
105
+ # name nobody can be signed in as the operator and the console has no
106
+ # portfolio to show.
107
+ raise IAToolkitException(
108
+ IAToolkitException.ErrorType.CONFIG_ERROR,
109
+ f"This deployment does not say who operates it. Set {self.SETTING_NAME} "
110
+ f"to the short name of the company that runs it.",
111
+ )
112
+
113
+ def billing_company_short_name(self) -> str:
114
+ """The company whose merchant account collects.
115
+
116
+ Falls back to the operator, because usually they are the same company.
117
+ Asked for only where money moves — a card being charged, a transfer notice
118
+ being signed, a webhook secret being read — so an installation that
119
+ collects from nobody never has to answer it.
120
+ """
121
+ explicit = (os.getenv(self.BILLING_SETTING_NAME) or "").strip().lower()
122
+ if explicit:
123
+ return explicit
124
+
125
+ try:
126
+ return self.company_short_name()
127
+ except IAToolkitException:
128
+ # The message has to be about collecting, because that is what the
129
+ # caller was doing. Pointing at the operator setting here sends
130
+ # somebody to configure identity for a payment problem.
131
+ raise IAToolkitException(
132
+ IAToolkitException.ErrorType.CONFIG_ERROR,
133
+ f"This deployment has no company to collect with. Set "
134
+ f"{self.BILLING_SETTING_NAME} to the company whose merchant "
135
+ f"account charges, or {self.SETTING_NAME} if the operator collects.",
136
+ )
@@ -234,8 +234,34 @@ class StorageService:
234
234
  logging.error(error_msg)
235
235
  raise IAToolkitException(IAToolkitException.ErrorType.FILE_IO_ERROR, error_msg)
236
236
 
237
+ @staticmethod
238
+ def _validate_storage_key(storage_key: str) -> str:
239
+ """
240
+ Defensive check shared by every read/delete: a storage key must never
241
+ walk up with `..` (or carry NUL bytes). Absolute paths are deliberately
242
+ left to the connector - LocalFileConnector confines them to its root,
243
+ and its list_files() legitimately hands back absolute paths that flow
244
+ into here. Callers that accept keys from untrusted input additionally
245
+ enforce the `companies/<tenant>/` prefix (see
246
+ AttachmentResolutionService._validate_existing_storage_key); this guard
247
+ is the floor that applies even to internally sourced keys.
248
+ """
249
+ normalized = str(storage_key or "").strip()
250
+ segments = normalized.replace("\\", "/").split("/")
251
+ if (
252
+ not normalized
253
+ or "\x00" in normalized
254
+ or any(segment in {".", ".."} for segment in segments)
255
+ ):
256
+ raise IAToolkitException(
257
+ IAToolkitException.ErrorType.INVALID_PARAMETER,
258
+ "Invalid storage key.",
259
+ )
260
+ return normalized
261
+
237
262
  def get_document_content(self, company_short_name: str, storage_key: str) -> bytes:
238
263
  """Retrieves raw content via the configured connector."""
264
+ storage_key = self._validate_storage_key(storage_key)
239
265
  try:
240
266
  connector = self._get_connector(company_short_name)
241
267
  return connector.get_file_content(storage_key)
@@ -247,6 +273,7 @@ class StorageService:
247
273
  """
248
274
  Deletes a document from the configured storage.
249
275
  """
276
+ storage_key = self._validate_storage_key(storage_key)
250
277
  try:
251
278
  connector = self._get_connector(company_short_name)
252
279
  connector.delete_file(storage_key)
@@ -3,6 +3,65 @@ let isRequestInProgress = false;
3
3
  let abortController = null;
4
4
  let selectedPrompt = null; // Will hold a lightweight prompt object
5
5
 
6
+ // Allowlist-style sanitizer for the HTML answer returned by the backend. The
7
+ // server already sanitizes it (LLMClientService.format_html -> nh3), but this is
8
+ // the last line before .append()/.html() parse it into the live DOM, and the
9
+ // model echoes tenant data (SQL rows, RAG documents, tool results) where an
10
+ // <img onerror> or javascript: link could be planted. Parses into an inert
11
+ // document, drops executable elements, event-handler attributes and
12
+ // script-bearing URLs, and returns the surviving markup.
13
+ function sanitizeHtmlFragment(html) {
14
+ const source = String(html || '');
15
+ if (!source) return '';
16
+ if (typeof DOMParser === 'undefined') {
17
+ return $('<div>').text(source).html();
18
+ }
19
+
20
+ const BLOCKED_TAGS = new Set([
21
+ 'script', 'style', 'iframe', 'frame', 'frameset', 'object', 'embed', 'applet',
22
+ 'link', 'meta', 'base', 'form', 'input', 'button', 'textarea', 'select', 'option',
23
+ 'svg', 'math', 'template', 'noscript',
24
+ ]);
25
+ const URL_ATTRS = new Set(['href', 'src', 'xlink:href', 'action', 'formaction', 'srcset', 'poster', 'background', 'data']);
26
+
27
+ const isUnsafeUrl = (value, attrName, tagName) => {
28
+ // Strip whitespace/control chars browsers ignore ("java\tscript:").
29
+ const normalized = String(value || '').replace(/[\u0000-\u0020\u007f-\u00a0]/g, '').toLowerCase();
30
+ if (normalized.startsWith('javascript:') || normalized.startsWith('vbscript:')) return true;
31
+ if (normalized.startsWith('data:')) {
32
+ return !(tagName === 'img' && attrName === 'src' && normalized.startsWith('data:image/'));
33
+ }
34
+ return false;
35
+ };
36
+
37
+ const doc = new DOMParser().parseFromString(source, 'text/html');
38
+ const walk = (node) => {
39
+ Array.from(node.children).forEach((el) => {
40
+ const tag = el.tagName.toLowerCase();
41
+ if (BLOCKED_TAGS.has(tag)) {
42
+ el.remove();
43
+ return;
44
+ }
45
+ Array.from(el.attributes).forEach((attr) => {
46
+ const name = attr.name.toLowerCase();
47
+ if (name.startsWith('on') || name === 'style' || name === 'srcdoc') {
48
+ el.removeAttribute(attr.name);
49
+ return;
50
+ }
51
+ if (URL_ATTRS.has(name) && isUnsafeUrl(attr.value, name, tag)) {
52
+ el.removeAttribute(attr.name);
53
+ }
54
+ });
55
+ if (tag === 'a') {
56
+ el.setAttribute('rel', 'noopener noreferrer');
57
+ }
58
+ walk(el);
59
+ });
60
+ };
61
+ walk(doc.body);
62
+ return doc.body.innerHTML;
63
+ }
64
+
6
65
  $(document).ready(function () {
7
66
  // Si viene un Token retornado por login con APY-KEY se gatilla el redeem a una sesion de flask
8
67
  if (window.redeemToken) {
@@ -177,7 +236,7 @@ function processBotResponse(responseData) {
177
236
  // A. Texto: Usamos 'answer' porque contiene el HTML procesado y limpio del backend.
178
237
  // Evitamos usar content_parts[type=text] porque contiene el JSON crudo del LLM.
179
238
  if (responseData.answer) {
180
- answerSection.append(responseData.answer);
239
+ answerSection.append(sanitizeHtmlFragment(responseData.answer));
181
240
  }
182
241
 
183
242
  // B. Imágenes: Iteramos content_parts buscando SOLO imágenes para adjuntarlas.
@@ -207,7 +266,8 @@ function processBotResponse(responseData) {
207
266
  toastr.error('Memoria no está disponible.');
208
267
  return;
209
268
  }
210
- const plainText = $('<div>').html(responseData.answer || '').text().trim();
269
+ // Even a detached element parses <img src onerror> and fires it - sanitize first.
270
+ const plainText = $('<div>').html(sanitizeHtmlFragment(responseData.answer || '')).text().trim();
211
271
  await window.saveItemToMemory({
212
272
  item_type: 'chat_assistant_message',
213
273
  content_text: plainText,
@@ -74,11 +74,31 @@ class PromptApiView(MethodView):
74
74
  f"unexpected error getting company prompts: {e}")
75
75
  return jsonify({"error_message": str(e)}), 500
76
76
 
77
+ def _require_admin_auth(self, company_short_name: str) -> dict | tuple:
78
+ """
79
+ Prompt content is a Jinja template rendered server-side for every user
80
+ of the tenant, so writing it is an admin capability: even sandboxed, a
81
+ template author can shape what every agent says and does. Regular chat
82
+ users / plain API keys pass verify_for_company but must not get here.
83
+ """
84
+ auth_result = self.auth_service.verify_for_company(company_short_name)
85
+ if not auth_result.get("success"):
86
+ status_code = auth_result.get("status_code", 401)
87
+ if status_code == 403:
88
+ return jsonify({"error": "Forbidden"}), 403
89
+ return jsonify(auth_result), status_code
90
+
91
+ role = str(auth_result.get("user_role") or "").strip().lower()
92
+ if role not in {"admin", "owner"}:
93
+ return jsonify({"error": "Forbidden"}), 403
94
+
95
+ return auth_result
96
+
77
97
  def put(self, company_short_name, prompt_name):
78
98
  try:
79
- auth_result = self.auth_service.verify_for_company(company_short_name)
80
- if not auth_result.get("success"):
81
- return jsonify(auth_result), 401
99
+ auth_result = self._require_admin_auth(company_short_name)
100
+ if isinstance(auth_result, tuple):
101
+ return auth_result
82
102
 
83
103
  data = request.get_json()
84
104
 
@@ -93,9 +113,9 @@ class PromptApiView(MethodView):
93
113
  def post(self, company_short_name, prompt_name=None):
94
114
  """Creates a new prompt."""
95
115
  try:
96
- auth_result = self.auth_service.verify_for_company(company_short_name)
97
- if not auth_result.get("success"):
98
- return jsonify(auth_result), 401
116
+ auth_result = self._require_admin_auth(company_short_name)
117
+ if isinstance(auth_result, tuple):
118
+ return auth_result
99
119
 
100
120
  data = request.get_json() or {}
101
121