iatoolkit 2.46.1__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 (253) hide show
  1. {iatoolkit-2.46.1/src/iatoolkit.egg-info → iatoolkit-2.47.0}/PKG-INFO +2 -1
  2. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/requirements.txt +1 -0
  3. {iatoolkit-2.46.1 → 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.1 → iatoolkit-2.47.0}/src/iatoolkit/common/util.py +12 -3
  6. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/infra/connectors/local_file_connector.py +27 -3
  7. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/llm_client_service.py +14 -4
  8. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/storage_service.py +27 -0
  9. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/static/js/chat_main.js +62 -2
  10. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/views/prompt_api_view.py +26 -6
  11. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/views/tool_api_view.py +29 -9
  12. {iatoolkit-2.46.1 → iatoolkit-2.47.0/src/iatoolkit.egg-info}/PKG-INFO +2 -1
  13. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit.egg-info/SOURCES.txt +1 -0
  14. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit.egg-info/requires.txt +1 -0
  15. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/LICENSE +0 -0
  16. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/LICENSE_COMMUNITY.md +0 -0
  17. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/pyproject.toml +0 -0
  18. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/readme.md +0 -0
  19. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/setup.cfg +0 -0
  20. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/base_company.py +0 -0
  21. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/cli_commands.py +0 -0
  22. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/common/__init__.py +0 -0
  23. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/common/bridge_validation.py +0 -0
  24. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/common/exceptions.py +0 -0
  25. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/common/interfaces/__init__.py +0 -0
  26. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/common/interfaces/asset_storage.py +0 -0
  27. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/common/interfaces/database_provider.py +0 -0
  28. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/common/interfaces/memory_compilation_trigger.py +0 -0
  29. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/common/interfaces/memory_lint_trigger.py +0 -0
  30. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/common/interfaces/secret_provider.py +0 -0
  31. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/common/interfaces/signup_policy_resolver.py +0 -0
  32. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/common/interfaces/web_search_provider.py +0 -0
  33. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/common/model_registry.py +0 -0
  34. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/common/routes.py +0 -0
  35. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/common/secret_resolver.py +0 -0
  36. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/common/session_manager.py +0 -0
  37. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/company_registry.py +0 -0
  38. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/config/__init__.py +0 -0
  39. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/config/llm_capabilities.yaml +0 -0
  40. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/channel_business_identity.prompt +0 -0
  41. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/channel_conversation_continuity.prompt +0 -0
  42. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/channel_response_style.prompt +0 -0
  43. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/chat_state_rules.prompt +0 -0
  44. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/chat_user_profile.prompt +0 -0
  45. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/core_identity.prompt +0 -0
  46. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/email_output.prompt +0 -0
  47. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/file_download_output.prompt +0 -0
  48. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/format_styles.prompt +0 -0
  49. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/html_structures.prompt +0 -0
  50. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/links_documents.prompt +0 -0
  51. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/memory_usage.prompt +0 -0
  52. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/multimodal_basics.prompt +0 -0
  53. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/output_basics.prompt +0 -0
  54. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/presentation_formatting.prompt +0 -0
  55. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/query_main.prompt +0 -0
  56. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/sql_aggregation_scope.prompt +0 -0
  57. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/sql_casting.prompt +0 -0
  58. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/sql_core.prompt +0 -0
  59. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/sql_jsonb.prompt +0 -0
  60. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/sql_mysql_casting.prompt +0 -0
  61. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/sql_mysql_json.prompt +0 -0
  62. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/sql_redshift_basics.prompt +0 -0
  63. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/sql_redshift_types.prompt +0 -0
  64. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/sql_rules.prompt +0 -0
  65. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/tool_html_passthrough.prompt +0 -0
  66. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts_pack.yaml +0 -0
  67. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/config/system_tools_pack.yaml +0 -0
  68. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/core.py +0 -0
  69. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/infra/__init__.py +0 -0
  70. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/infra/brevo_mail_app.py +0 -0
  71. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/infra/call_service.py +0 -0
  72. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/infra/connectors/__init__.py +0 -0
  73. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/infra/connectors/file_connector.py +0 -0
  74. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/infra/connectors/file_connector_factory.py +0 -0
  75. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/infra/connectors/google_cloud_storage_connector.py +0 -0
  76. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/infra/connectors/google_drive_connector.py +0 -0
  77. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/infra/connectors/s3_connector.py +0 -0
  78. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/infra/google_auth_client.py +0 -0
  79. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/infra/google_chat_app.py +0 -0
  80. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/infra/inference_embeddings_client.py +0 -0
  81. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/infra/inference_handler.py +0 -0
  82. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/infra/jina_embeddings_client.py +0 -0
  83. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/infra/llm_gateway_resolver.py +0 -0
  84. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/infra/llm_providers/__init__.py +0 -0
  85. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/infra/llm_providers/anthropic_adapter.py +0 -0
  86. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/infra/llm_providers/deepseek_adapter.py +0 -0
  87. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/infra/llm_providers/gemini_adapter.py +0 -0
  88. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/infra/llm_providers/openai_adapter.py +0 -0
  89. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/infra/llm_providers/openai_compatible_chat_adapter.py +0 -0
  90. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/infra/llm_providers/openrouter_adapter.py +0 -0
  91. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/infra/llm_proxy.py +0 -0
  92. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/infra/llm_response.py +0 -0
  93. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/infra/redis_session_manager.py +0 -0
  94. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/locales/en.yaml +0 -0
  95. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/locales/es.yaml +0 -0
  96. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/repositories/__init__.py +0 -0
  97. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/repositories/api_key_repo.py +0 -0
  98. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/repositories/bridge_repo.py +0 -0
  99. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/repositories/database_manager.py +0 -0
  100. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/repositories/document_repo.py +0 -0
  101. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/repositories/env_secret_provider.py +0 -0
  102. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/repositories/filesystem_asset_repository.py +0 -0
  103. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/repositories/knowledge_wiki_repo.py +0 -0
  104. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/repositories/llm_query_repo.py +0 -0
  105. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/repositories/mcp_token_repo.py +0 -0
  106. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/repositories/memory_repo.py +0 -0
  107. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/repositories/models.py +0 -0
  108. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/repositories/profile_repo.py +0 -0
  109. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/repositories/prompt_resource_repo.py +0 -0
  110. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/repositories/sql_dataset_repo.py +0 -0
  111. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/repositories/sql_source_repo.py +0 -0
  112. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/repositories/vs_repo.py +0 -0
  113. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/runtime_logging.py +0 -0
  114. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/__init__.py +0 -0
  115. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/api_key_service.py +0 -0
  116. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/attachment_policy_service.py +0 -0
  117. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/auth_service.py +0 -0
  118. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/benchmark_service.py +0 -0
  119. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/branding_service.py +0 -0
  120. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/company_context_service.py +0 -0
  121. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/configuration_service.py +0 -0
  122. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/context_builder_service.py +0 -0
  123. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/dispatcher_service.py +0 -0
  124. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/embedding_service.py +0 -0
  125. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/excel_service.py +0 -0
  126. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/file_processor_service.py +0 -0
  127. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/history_manager_service.py +0 -0
  128. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/http_tool_service.py +0 -0
  129. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/i18n_service.py +0 -0
  130. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/inference_service.py +0 -0
  131. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/jwt_service.py +0 -0
  132. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/knowledge_base_service.py +0 -0
  133. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/language_service.py +0 -0
  134. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/license_service.py +0 -0
  135. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/mail_service.py +0 -0
  136. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/markdown_wiki_service.py +0 -0
  137. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/mcp_token_service.py +0 -0
  138. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/memory_compiler_service.py +0 -0
  139. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/memory_lint_service.py +0 -0
  140. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/memory_lookup_policy_service.py +0 -0
  141. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/memory_service.py +0 -0
  142. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/memory_wiki_service.py +0 -0
  143. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/noop_memory_compilation_trigger.py +0 -0
  144. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/noop_memory_lint_trigger.py +0 -0
  145. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/operator_identity_service.py +0 -0
  146. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/parsers/__init__.py +0 -0
  147. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/parsers/contracts.py +0 -0
  148. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/parsers/image_normalizer.py +0 -0
  149. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/parsers/parsing_service.py +0 -0
  150. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/parsers/pdf_ocr_detection.py +0 -0
  151. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/parsers/provider_factory.py +0 -0
  152. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/parsers/provider_resolver.py +0 -0
  153. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/parsers/providers/__init__.py +0 -0
  154. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/parsers/providers/basic_provider.py +0 -0
  155. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/parsers/providers/docling_provider.py +0 -0
  156. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/parsers/validator.py +0 -0
  157. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/pdf_service.py +0 -0
  158. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/profile_service.py +0 -0
  159. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/prompt_resource_service.py +0 -0
  160. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/prompt_service.py +0 -0
  161. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/query_service.py +0 -0
  162. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/signup_policy_resolver.py +0 -0
  163. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/sql_dataset_service.py +0 -0
  164. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/sql_service.py +0 -0
  165. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/sql_source_service.py +0 -0
  166. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/structured_output_service.py +0 -0
  167. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/system_prompt_catalog.py +0 -0
  168. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/system_tools.py +0 -0
  169. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/telemetry_service.py +0 -0
  170. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/tenant_wiki_service.py +0 -0
  171. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/tool_output_contract.py +0 -0
  172. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/tool_service.py +0 -0
  173. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/user_feedback_service.py +0 -0
  174. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/user_session_context_service.py +0 -0
  175. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/visual_kb_service.py +0 -0
  176. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/visual_tool_service.py +0 -0
  177. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/warmup_service.py +0 -0
  178. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/web_search/__init__.py +0 -0
  179. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/web_search/provider_factory.py +0 -0
  180. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/web_search/providers/__init__.py +0 -0
  181. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/web_search/providers/brave_provider.py +0 -0
  182. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/services/web_search_service.py +0 -0
  183. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/static/images/fernando.jpeg +0 -0
  184. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/static/images/iatoolkit_core.png +0 -0
  185. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/static/images/iatoolkit_logo.png +0 -0
  186. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/static/js/chat_feedback_button.js +0 -0
  187. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/static/js/chat_filepond.js +0 -0
  188. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/static/js/chat_help_content.js +0 -0
  189. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/static/js/chat_history_button.js +0 -0
  190. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/static/js/chat_logout_button.js +0 -0
  191. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/static/js/chat_memory_button.js +0 -0
  192. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/static/js/chat_model_selector.js +0 -0
  193. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/static/js/chat_onboarding_button.js +0 -0
  194. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/static/js/chat_prompt_manager.js +0 -0
  195. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/static/js/chat_reload_button.js +0 -0
  196. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/static/styles/chat_iatoolkit.css +0 -0
  197. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/static/styles/chat_modal.css +0 -0
  198. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/static/styles/chat_public.css +0 -0
  199. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/static/styles/documents.css +0 -0
  200. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/static/styles/landing_page.css +0 -0
  201. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/static/styles/llm_output.css +0 -0
  202. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/static/styles/onboarding.css +0 -0
  203. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/templates/_company_header.html +0 -0
  204. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/templates/_login_widget.html +0 -0
  205. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/templates/account.html +0 -0
  206. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/templates/base.html +0 -0
  207. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/templates/change_password.html +0 -0
  208. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/templates/chat.html +0 -0
  209. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/templates/chat_memory_modal.html +0 -0
  210. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/templates/chat_modals.html +0 -0
  211. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/templates/error.html +0 -0
  212. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/templates/forgot_password.html +0 -0
  213. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/templates/home_hosted_default.html +0 -0
  214. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/templates/memory/wiki_schema.md +0 -0
  215. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/templates/onboarding_shell.html +0 -0
  216. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/templates/pdf/base.html +0 -0
  217. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/templates/pdf/letter.html +0 -0
  218. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/templates/pdf/report.html +0 -0
  219. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/templates/pdf/simple.html +0 -0
  220. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/templates/signup.html +0 -0
  221. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/views/__init__.py +0 -0
  222. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/views/account_view.py +0 -0
  223. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/views/api_key_api_view.py +0 -0
  224. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/views/base_login_view.py +0 -0
  225. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/views/categories_api_view.py +0 -0
  226. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/views/change_password_view.py +0 -0
  227. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/views/chat_context_preview_api_view.py +0 -0
  228. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/views/chat_view.py +0 -0
  229. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/views/configuration_api_view.py +0 -0
  230. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/views/connectors_api_view.py +0 -0
  231. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/views/embedding_api_view.py +0 -0
  232. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/views/forgot_password_view.py +0 -0
  233. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/views/help_content_api_view.py +0 -0
  234. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/views/history_api_view.py +0 -0
  235. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/views/home_view.py +0 -0
  236. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/views/init_context_api_view.py +0 -0
  237. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/views/llmquery_api_view.py +0 -0
  238. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/views/load_document_api_view.py +0 -0
  239. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/views/login_view.py +0 -0
  240. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/views/logout_api_view.py +0 -0
  241. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/views/memory_api_view.py +0 -0
  242. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/views/profile_api_view.py +0 -0
  243. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/views/prompt_context_preview_api_view.py +0 -0
  244. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/views/prompt_resource_api_view.py +0 -0
  245. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/views/rag_api_view.py +0 -0
  246. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/views/root_redirect_view.py +0 -0
  247. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/views/signup_view.py +0 -0
  248. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/views/static_page_view.py +0 -0
  249. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/views/user_feedback_api_view.py +0 -0
  250. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/views/users_api_view.py +0 -0
  251. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit/views/verify_user_view.py +0 -0
  252. {iatoolkit-2.46.1 → iatoolkit-2.47.0}/src/iatoolkit.egg-info/dependency_links.txt +0 -0
  253. {iatoolkit-2.46.1 → 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.1
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.1"
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)
@@ -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:
@@ -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
 
@@ -67,14 +67,34 @@ class ToolApiView(MethodView):
67
67
  logging.exception(f"Tool API Error (GET): {e}")
68
68
  return jsonify({"error": "Internal server error"}), 500
69
69
 
70
+ def _require_admin_auth(self, company_short_name: str) -> dict | tuple:
71
+ """
72
+ Creating or changing a tool defines code the agent will execute for every
73
+ user of the tenant (HTTP targets, inference endpoints, execution_config),
74
+ so it's an admin capability. Regular chat users and plain API keys pass
75
+ verify_for_company but must not be able to write tool definitions.
76
+ """
77
+ auth_result = self.auth_service.verify_for_company(company_short_name)
78
+ if not auth_result.get("success"):
79
+ status_code = auth_result.get("status_code", 401)
80
+ if status_code == 403:
81
+ return jsonify({"error": "Forbidden"}), 403
82
+ return jsonify(auth_result), status_code
83
+
84
+ role = str(auth_result.get("user_role") or "").strip().lower()
85
+ if role not in {"admin", "owner"}:
86
+ return jsonify({"error": "Forbidden"}), 403
87
+
88
+ return auth_result
89
+
70
90
  def post(self, company_short_name: str):
71
91
  """
72
92
  POST /<company>/api/tools -> Create a new tool
73
93
  Body: { "name": "...", "description": "...", "tool_type": "...", ... }
74
94
  """
75
- auth_result = self.auth_service.verify_for_company(company_short_name)
76
- if not auth_result.get("success"):
77
- return jsonify(auth_result), auth_result.get("status_code", 401)
95
+ auth_result = self._require_admin_auth(company_short_name)
96
+ if isinstance(auth_result, tuple):
97
+ return auth_result
78
98
 
79
99
  try:
80
100
  data = request.get_json() or {}
@@ -96,9 +116,9 @@ class ToolApiView(MethodView):
96
116
  """
97
117
  PUT /<company>/api/tools/<id> -> Update an existing tool
98
118
  """
99
- auth_result = self.auth_service.verify_for_company(company_short_name)
100
- if not auth_result.get("success"):
101
- return jsonify(auth_result), auth_result.get("status_code", 401)
119
+ auth_result = self._require_admin_auth(company_short_name)
120
+ if isinstance(auth_result, tuple):
121
+ return auth_result
102
122
 
103
123
  try:
104
124
  data = request.get_json() or {}
@@ -123,9 +143,9 @@ class ToolApiView(MethodView):
123
143
  """
124
144
  DELETE /<company>/api/tools/<id> -> Delete a tool
125
145
  """
126
- auth_result = self.auth_service.verify_for_company(company_short_name)
127
- if not auth_result.get("success"):
128
- return jsonify(auth_result), auth_result.get("status_code", 401)
146
+ auth_result = self._require_admin_auth(company_short_name)
147
+ if isinstance(auth_result, tuple):
148
+ return auth_result
129
149
 
130
150
  try:
131
151
  actor = auth_result.get("user_identifier")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iatoolkit
3
- Version: 2.46.1
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
@@ -21,6 +21,7 @@ src/iatoolkit/common/model_registry.py
21
21
  src/iatoolkit/common/routes.py
22
22
  src/iatoolkit/common/secret_resolver.py
23
23
  src/iatoolkit/common/session_manager.py
24
+ src/iatoolkit/common/url_safety.py
24
25
  src/iatoolkit/common/util.py
25
26
  src/iatoolkit/common/interfaces/__init__.py
26
27
  src/iatoolkit/common/interfaces/asset_storage.py
@@ -41,6 +41,7 @@ langchain==0.3.19
41
41
  langchain-core==0.3.35
42
42
  langchain-text-splitters==0.3.6
43
43
  markdown2==2.5.3
44
+ nh3==0.3.7
44
45
  openai==2.8.1
45
46
  openpyxl==3.1.5
46
47
  pandas==2.3.1
File without changes
File without changes
File without changes
File without changes