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.
- {iatoolkit-2.46.0/src/iatoolkit.egg-info → iatoolkit-2.47.0}/PKG-INFO +2 -1
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/requirements.txt +1 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/__init__.py +1 -1
- iatoolkit-2.47.0/src/iatoolkit/common/url_safety.py +163 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/common/util.py +12 -3
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/connectors/local_file_connector.py +27 -3
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/llm_providers/openai_adapter.py +6 -2
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/llm_client_service.py +14 -4
- iatoolkit-2.47.0/src/iatoolkit/services/operator_identity_service.py +136 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/storage_service.py +27 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/static/js/chat_main.js +62 -2
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/prompt_api_view.py +26 -6
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/tool_api_view.py +29 -9
- {iatoolkit-2.46.0 → iatoolkit-2.47.0/src/iatoolkit.egg-info}/PKG-INFO +2 -1
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit.egg-info/SOURCES.txt +1 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit.egg-info/requires.txt +1 -0
- iatoolkit-2.46.0/src/iatoolkit/services/operator_identity_service.py +0 -114
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/LICENSE +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/LICENSE_COMMUNITY.md +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/pyproject.toml +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/readme.md +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/setup.cfg +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/base_company.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/cli_commands.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/common/__init__.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/common/bridge_validation.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/common/exceptions.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/common/interfaces/__init__.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/common/interfaces/asset_storage.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/common/interfaces/database_provider.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/common/interfaces/memory_compilation_trigger.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/common/interfaces/memory_lint_trigger.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/common/interfaces/secret_provider.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/common/interfaces/signup_policy_resolver.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/common/interfaces/web_search_provider.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/common/model_registry.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/common/routes.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/common/secret_resolver.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/common/session_manager.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/company_registry.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/__init__.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/llm_capabilities.yaml +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/channel_business_identity.prompt +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/channel_conversation_continuity.prompt +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/channel_response_style.prompt +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/chat_state_rules.prompt +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/chat_user_profile.prompt +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/core_identity.prompt +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/email_output.prompt +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/file_download_output.prompt +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/format_styles.prompt +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/html_structures.prompt +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/links_documents.prompt +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/memory_usage.prompt +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/multimodal_basics.prompt +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/output_basics.prompt +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/presentation_formatting.prompt +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/query_main.prompt +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/sql_aggregation_scope.prompt +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/sql_casting.prompt +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/sql_core.prompt +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/sql_jsonb.prompt +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/sql_mysql_casting.prompt +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/sql_mysql_json.prompt +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/sql_redshift_basics.prompt +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/sql_redshift_types.prompt +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/sql_rules.prompt +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts/tool_html_passthrough.prompt +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_prompts_pack.yaml +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/config/system_tools_pack.yaml +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/core.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/__init__.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/brevo_mail_app.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/call_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/connectors/__init__.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/connectors/file_connector.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/connectors/file_connector_factory.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/connectors/google_cloud_storage_connector.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/connectors/google_drive_connector.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/connectors/s3_connector.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/google_auth_client.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/google_chat_app.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/inference_embeddings_client.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/inference_handler.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/jina_embeddings_client.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/llm_gateway_resolver.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/llm_providers/__init__.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/llm_providers/anthropic_adapter.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/llm_providers/deepseek_adapter.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/llm_providers/gemini_adapter.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/llm_providers/openai_compatible_chat_adapter.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/llm_providers/openrouter_adapter.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/llm_proxy.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/llm_response.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/redis_session_manager.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/locales/en.yaml +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/locales/es.yaml +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/repositories/__init__.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/repositories/api_key_repo.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/repositories/bridge_repo.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/repositories/database_manager.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/repositories/document_repo.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/repositories/env_secret_provider.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/repositories/filesystem_asset_repository.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/repositories/knowledge_wiki_repo.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/repositories/llm_query_repo.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/repositories/mcp_token_repo.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/repositories/memory_repo.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/repositories/models.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/repositories/profile_repo.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/repositories/prompt_resource_repo.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/repositories/sql_dataset_repo.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/repositories/sql_source_repo.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/repositories/vs_repo.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/runtime_logging.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/__init__.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/api_key_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/attachment_policy_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/auth_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/benchmark_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/branding_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/company_context_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/configuration_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/context_builder_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/dispatcher_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/embedding_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/excel_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/file_processor_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/history_manager_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/http_tool_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/i18n_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/inference_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/jwt_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/knowledge_base_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/language_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/license_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/mail_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/markdown_wiki_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/mcp_token_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/memory_compiler_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/memory_lint_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/memory_lookup_policy_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/memory_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/memory_wiki_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/noop_memory_compilation_trigger.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/noop_memory_lint_trigger.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/parsers/__init__.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/parsers/contracts.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/parsers/image_normalizer.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/parsers/parsing_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/parsers/pdf_ocr_detection.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/parsers/provider_factory.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/parsers/provider_resolver.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/parsers/providers/__init__.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/parsers/providers/basic_provider.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/parsers/providers/docling_provider.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/parsers/validator.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/pdf_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/profile_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/prompt_resource_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/prompt_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/query_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/signup_policy_resolver.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/sql_dataset_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/sql_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/sql_source_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/structured_output_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/system_prompt_catalog.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/system_tools.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/telemetry_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/tenant_wiki_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/tool_output_contract.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/tool_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/user_feedback_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/user_session_context_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/visual_kb_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/visual_tool_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/warmup_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/web_search/__init__.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/web_search/provider_factory.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/web_search/providers/__init__.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/web_search/providers/brave_provider.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/services/web_search_service.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/static/images/fernando.jpeg +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/static/images/iatoolkit_core.png +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/static/images/iatoolkit_logo.png +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/static/js/chat_feedback_button.js +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/static/js/chat_filepond.js +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/static/js/chat_help_content.js +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/static/js/chat_history_button.js +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/static/js/chat_logout_button.js +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/static/js/chat_memory_button.js +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/static/js/chat_model_selector.js +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/static/js/chat_onboarding_button.js +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/static/js/chat_prompt_manager.js +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/static/js/chat_reload_button.js +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/static/styles/chat_iatoolkit.css +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/static/styles/chat_modal.css +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/static/styles/chat_public.css +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/static/styles/documents.css +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/static/styles/landing_page.css +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/static/styles/llm_output.css +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/static/styles/onboarding.css +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/templates/_company_header.html +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/templates/_login_widget.html +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/templates/account.html +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/templates/base.html +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/templates/change_password.html +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/templates/chat.html +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/templates/chat_memory_modal.html +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/templates/chat_modals.html +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/templates/error.html +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/templates/forgot_password.html +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/templates/home_hosted_default.html +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/templates/memory/wiki_schema.md +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/templates/onboarding_shell.html +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/templates/pdf/base.html +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/templates/pdf/letter.html +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/templates/pdf/report.html +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/templates/pdf/simple.html +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/templates/signup.html +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/__init__.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/account_view.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/api_key_api_view.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/base_login_view.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/categories_api_view.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/change_password_view.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/chat_context_preview_api_view.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/chat_view.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/configuration_api_view.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/connectors_api_view.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/embedding_api_view.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/forgot_password_view.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/help_content_api_view.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/history_api_view.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/home_view.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/init_context_api_view.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/llmquery_api_view.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/load_document_api_view.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/login_view.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/logout_api_view.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/memory_api_view.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/profile_api_view.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/prompt_context_preview_api_view.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/prompt_resource_api_view.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/rag_api_view.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/root_redirect_view.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/signup_view.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/static_page_view.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/user_feedback_api_view.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/users_api_view.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/views/verify_user_view.py +0 -0
- {iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit.egg-info/dependency_links.txt +0 -0
- {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.
|
|
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
|
|
@@ -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
|
|
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 =
|
|
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 =
|
|
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
|
{iatoolkit-2.46.0 → iatoolkit-2.47.0}/src/iatoolkit/infra/connectors/local_file_connector.py
RENAMED
|
@@ -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(
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
100
|
-
|
|
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
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
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
|
-
|
|
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.
|
|
80
|
-
if
|
|
81
|
-
return
|
|
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.
|
|
97
|
-
if
|
|
98
|
-
return
|
|
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
|
|