chatflow-agent 0.3.0__tar.gz → 0.3.2__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 (59) hide show
  1. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/CHANGELOG.md +40 -0
  2. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/PKG-INFO +41 -1
  3. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/README.md +40 -0
  4. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/pyproject.toml +1 -1
  5. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/src/chatflow_agent/__init__.py +1 -1
  6. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/src/chatflow_agent/channels/__init__.py +2 -1
  7. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/src/chatflow_agent/channels/base.py +36 -0
  8. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/src/chatflow_agent/channels/telegram.py +27 -2
  9. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/src/chatflow_agent/channels/whatsapp.py +54 -40
  10. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/src/chatflow_agent/core/engines/anthropic.py +57 -12
  11. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/src/chatflow_agent/core/engines/openai_compatible.py +28 -3
  12. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/src/chatflow_agent/core/runner.py +11 -4
  13. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/src/chatflow_agent/core/tools.py +26 -5
  14. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/tests/test_multi_engine.py +191 -2
  15. chatflow_agent-0.3.2/tests/test_runner_security.py +168 -0
  16. chatflow_agent-0.3.2/tests/test_telegram.py +239 -0
  17. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/tests/test_tools.py +28 -0
  18. chatflow_agent-0.3.2/tests/test_whatsapp_security.py +406 -0
  19. chatflow_agent-0.3.0/tests/test_telegram.py +0 -82
  20. chatflow_agent-0.3.0/tests/test_whatsapp_security.py +0 -190
  21. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/.github/workflows/publish.yml +0 -0
  22. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/.github/workflows/test.yml +0 -0
  23. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/.gitignore +0 -0
  24. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/LICENSE +0 -0
  25. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/examples/cli_multiagent.py +0 -0
  26. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/examples/local_gemma_cli.py +0 -0
  27. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/examples/multi_provider_swarm.py +0 -0
  28. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/examples/telegram_persistent_bot.py +0 -0
  29. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/examples/telegram_triage.py +0 -0
  30. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/examples/webhook_api_service.py +0 -0
  31. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/examples/whatsapp_persistent_bot.py +0 -0
  32. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/examples/whatsapp_service.py +0 -0
  33. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/src/chatflow_agent/channels/cli.py +0 -0
  34. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/src/chatflow_agent/core/__init__.py +0 -0
  35. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/src/chatflow_agent/core/agent.py +0 -0
  36. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/src/chatflow_agent/core/engine.py +0 -0
  37. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/src/chatflow_agent/core/engines/__init__.py +0 -0
  38. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/src/chatflow_agent/core/engines/base.py +0 -0
  39. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/src/chatflow_agent/core/engines/factory.py +0 -0
  40. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/src/chatflow_agent/core/engines/gemini.py +0 -0
  41. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/src/chatflow_agent/core/memory.py +0 -0
  42. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/src/chatflow_agent/exceptions.py +0 -0
  43. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/src/chatflow_agent/memory.py +0 -0
  44. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/src/chatflow_agent/py.typed +0 -0
  45. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/src/chatflow_agent/types.py +0 -0
  46. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/tests/__init__.py +0 -0
  47. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/tests/test_agent.py +0 -0
  48. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/tests/test_cli_channel.py +0 -0
  49. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/tests/test_memory.py +0 -0
  50. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/tests/test_memory_integrity.py +0 -0
  51. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/tests/test_memory_sqlite.py +0 -0
  52. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/tests/test_resilience.py +0 -0
  53. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/tests/test_runner.py +0 -0
  54. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/tests/test_runner_rollback.py +0 -0
  55. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/tests/test_types.py +0 -0
  56. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/tests/test_whatsapp.py +0 -0
  57. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/tests/test_whatsapp_background.py +0 -0
  58. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/tests/test_whatsapp_resilience_outbound.py +0 -0
  59. {chatflow_agent-0.3.0 → chatflow_agent-0.3.2}/tests/test_whatsapp_split.py +0 -0
@@ -12,6 +12,26 @@ Versioning / Versionado: [Semantic Versioning](https://semver.org/spec/v2.0.0.ht
12
12
 
13
13
  ## English
14
14
 
15
+ ## [Unreleased]
16
+
17
+ ## [0.3.2] - 2026-09-29
18
+
19
+ ### Security
20
+ - **Tool Execution Error Sanitization (SEC-02):** Sanitized tool execution errors in `Runner` before exposing them to the LLM, returning a generic error with a unique `trace_id` UUID and logging the original traceback internally to prevent leakage of internal stack traces or environment variables (Issue #3).
21
+ - **Mask Phone Numbers in Exception Logs (SEC-03):** Automatically mask phone numbers (`_mask_phone`) in exception and fallback delivery logs in `WhatsAppChannel` for privacy compliance (Issue #5).
22
+ - **Admin-Only /reset in Telegram Groups (SEC-04):** Restricted the `/reset` command in Telegram group and supergroup chats strictly to chat creators and administrators (Issue #7).
23
+ - **Disabled FastAPI OpenAPI / Docs Endpoints by Default (SEC-05):** `WhatsAppChannel` now sets `docs_url=None`, `redoc_url=None`, and `openapi_url=None` by default to prevent internal schema and route exposure on public webhook endpoints. Interactive documentation can be optionally enabled for development via `enable_docs=True` (Issue #9).
24
+ - **Reverse Proxy Rate Limiting Recommendation (SEC-06):** Added production deployment guidance for placing Uvicorn behind a reverse proxy (e.g., Nginx with `limit_req_zone` or Cloudflare Rate Limiting) to protect `/webhook` and `/health` endpoints against flood attacks and CPU exhaustion from unauthenticated requests, avoiding in-app rate limiting that could drop legitimate bursts of Meta webhooks (Issue #10).
25
+ - **API Key Leakage Prevention on Custom Base URLs (SEC-07):** `OpenAIEngine` now prevents forwarding global environment credentials (`OPENAI_API_KEY`) to untrusted or custom remote endpoints. When a custom `base_url` (not matching official presets or localhost/127.0.0.1) is provided without an explicit `api_key`, a `ValueError` is raised, requiring an explicit `api_key` (or `api_key=""` if unauthenticated) (Issue #11).
26
+ - **Outbound Message Splitting in Telegram (SEC-08):** `TelegramChannel` now splits outbound responses exceeding Telegram's 4,096-character limit into progressive chunks using `split_message()`, preventing systematic delivery failures and error fallbacks on long LLM responses. `split_message()` now resides in `chatflow_agent.channels.base` and is re-exported from `whatsapp` for backwards compatibility (Issue #12).
27
+ - **Async vs Sync Tool Execution Clarification and Guard (SEC-09):** Documented that asynchronous tools must always be invoked via `await tool.execute_async(**kwargs)` in asynchronous environments (FastAPI, Runners, Telegram handlers). Calling `tool.execute()` on an async tool from within an active running event loop now immediately raises a clear `ToolExecutionError` explaining why synchronous re-entry of the running event loop is unsupported and directing the caller to `execute_async()` (Issue #13).
28
+ - **Retry and Backoff in AnthropicEngine (SEC-10):** `AnthropicEngine` now includes exponential backoff with jitter and retry handling for transient API errors (HTTP 429, 500, 502, 503, 504, 529 Overloaded, and connection errors `httpx.ConnectError`, `httpx.ConnectTimeout`). Honors the `Retry-After` header when provided (Issue #14).
29
+
30
+ ## [0.3.1] - 2026-09-25
31
+
32
+ ### Security
33
+ - **WhatsApp Webhook Request Body Size Limit (SEC-01):** Fixed vulnerability where inbound `POST /webhook` requests did not validate the payload size prior to buffering, enabling potential Denial of Service (DoS via Out-of-Memory / OOM). The endpoint now validates the `Content-Length` header before reading the body, enforcing a configurable limit (default: 1 MB via `DEFAULT_MAX_BODY_SIZE`) and rejecting oversized payloads with HTTP 413, missing headers with HTTP 411, and malformed headers with HTTP 400.
34
+
15
35
  ## [0.3.0] - 2026-09-21
16
36
 
17
37
  ### Security
@@ -47,6 +67,26 @@ If you are upgrading from v0.2.0:
47
67
 
48
68
  ## Español
49
69
 
70
+ ## [Unreleased]
71
+
72
+ ## [0.3.2] - 2026-09-29
73
+
74
+ ### Seguridad
75
+ - **Sanitización de Errores de Tools (SEC-02):** Se sanitizaron los errores de ejecución de herramientas en `Runner` antes de exponerlos al LLM, retornando un mensaje genérico con un UUID `trace_id` único y registrando el traceback internamente para prevenir la fuga de stack traces o variables de entorno (Issue #3).
76
+ - **Enmascaramiento de Teléfonos en Logs (SEC-03):** Se enmascaran automáticamente los números telefónicos (`_mask_phone`) en logs de excepciones y fallbacks en `WhatsAppChannel` para cumplimiento de privacidad (Issue #5).
77
+ - **Comando /reset Exclusivo para Administradores en Telegram (SEC-04):** Se restringió la ejecución del comando `/reset` en grupos y supergrupos de Telegram exclusivamente a creadores y administradores (Issue #7).
78
+ - **Desactivación de Endpoints OpenAPI / Docs de FastAPI por Defecto (SEC-05):** `WhatsAppChannel` ahora configura `docs_url=None`, `redoc_url=None` y `openapi_url=None` por defecto para prevenir la exposición de rutas y esquemas internos en endpoints públicos de webhook. La documentación interactiva puede habilitarse opcionalmente para desarrollo mediante `enable_docs=True` (Issue #9).
79
+ - **Recomendación de Rate Limiting con Reverse Proxy (SEC-06):** Se añadió documentación de despliegue en producción recomendando ubicar Uvicorn detrás de un reverse proxy (como Nginx con `limit_req_zone` o Cloudflare Rate Limiting) para proteger los endpoints `/webhook` y `/health` contra ataques de inundación y agotamiento de CPU por peticiones no autenticadas, evitando rate limiting a nivel aplicación que pudiera descartar ráfagas legítimas de webhooks de Meta (Issue #10).
80
+ - **Prevención de Fuga de Claves API en Base URLs Personalizadas (SEC-07):** `OpenAIEngine` ahora previene el reenvío de credenciales globales de entorno (`OPENAI_API_KEY`) a endpoints remotos personalizados o no oficiales. Cuando se proporciona un `base_url` personalizado (que no coincide con los presets oficiales ni con localhost/127.0.0.1) sin una `api_key` explícita, se levanta un `ValueError`, exigiendo una `api_key` explícita (o `api_key=""` si no requiere autenticación) (Issue #11).
81
+ - **División de Mensajes Salientes en Telegram (SEC-08):** `TelegramChannel` ahora divide las respuestas salientes que superan el límite de 4.096 caracteres de Telegram en fragmentos progresivos usando `split_message()`, previniendo fallos sistemáticos de entrega y respuestas de error en respuestas extensas de LLM. `split_message()` ahora reside en `chatflow_agent.channels.base` y se reexporta desde `whatsapp` para retrocompatibilidad (Issue #12).
82
+ - **Clarificación y Validación de Ejecución de Tools Asíncronas vs Síncronas (SEC-09):** Se documentó que las herramientas asíncronas deben invocarse siempre mediante `await tool.execute_async(**kwargs)` en entornos asíncronos (FastAPI, Runners, handlers de Telegram). Ejecutar `tool.execute()` sobre una herramienta asíncrona dentro de un bucle de eventos activo ahora levanta inmediatamente un `ToolExecutionError` claro explicando que la reentrada síncrona al bucle de eventos no está soportada e indicando el uso de `execute_async()` (Issue #13).
83
+ - **Reintentos y Backoff Exponencial en AnthropicEngine (SEC-10):** `AnthropicEngine` ahora incluye reintentos automáticos con backoff exponencial y jitter ante errores transitorios de la API (HTTP 429, 500, 502, 503, 504, 529 Sobrecargado, y errores de conexión `httpx.ConnectError`, `httpx.ConnectTimeout`). Respeta la cabecera `Retry-After` cuando está presente (Issue #14).
84
+
85
+ ## [0.3.1] - 2026-09-25
86
+
87
+ ### Seguridad
88
+ - **Límite de Tamaño en Request Body del Webhook de WhatsApp (SEC-01):** Se corrigió una vulnerabilidad donde las peticiones entrantes `POST /webhook` no validaban el tamaño del payload antes de cargarlo en memoria, permitiendo ataques de Denegación de Servicio por agotamiento de memoria (DoS / OOM). Ahora se valida el encabezado `Content-Length` antes de leer el cuerpo, aplicando un límite configurable (por defecto 1 MB mediante `DEFAULT_MAX_BODY_SIZE`) y rechazando payloads excesivos con HTTP 413, encabezados ausentes con HTTP 411 y encabezados malformados con HTTP 400.
89
+
50
90
  ## [0.3.0] - 2026-09-21
51
91
 
52
92
  ### Seguridad
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: chatflow-agent
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Lightweight, async multi-agent framework with native handoffs for Gemini, Gemma, Grok, OpenAI & Claude.
5
5
  Project-URL: Homepage, https://github.com/GustavoBaranda/chatflow-agent
6
6
  Project-URL: Source Code, https://github.com/GustavoBaranda/chatflow-agent
@@ -583,6 +583,26 @@ if __name__ == "__main__":
583
583
 
584
584
  > **Testing locally?** Use a tunnel like [ngrok](https://ngrok.com) (`ngrok http 8000`) or Cloudflare Tunnels to provide Meta with a public HTTPS URL (`https://your-domain.ngrok-free.app/webhook`).
585
585
 
586
+ > [!NOTE]
587
+ > **API Documentation in Development:** For security, FastAPI interactive documentation (`/docs`, `/redoc`, and `/openapi.json`) is disabled by default in production. To enable Swagger UI and OpenAPI schemas during local development or debugging, pass `enable_docs=True` when instantiating `WhatsAppChannel`:
588
+ > ```python
589
+ > channel = WhatsAppChannel(
590
+ > ...,
591
+ > enable_docs=True, # Enables /docs, /redoc, and /openapi.json for local development
592
+ > )
593
+ > ```
594
+
595
+ > [!TIP]
596
+ > **Production Rate Limiting & Reverse Proxy:** To protect `/webhook` and `/health` endpoints against flood attacks, denial of service (DoS), and CPU exhaustion from unauthenticated requests without risking dropping legitimate Meta webhook bursts at the application level, always place Uvicorn behind a reverse proxy in production (such as Nginx with `limit_req_zone` or Cloudflare Rate Limiting):
597
+ > ```nginx
598
+ > limit_req_zone $binary_remote_addr zone=whatsapp_limit:10m rate=10r/s;
599
+ > location /webhook {
600
+ > limit_req zone=whatsapp_limit burst=20 nodelay;
601
+ > proxy_pass http://127.0.0.1:8000;
602
+ > proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
603
+ > }
604
+ > ```
605
+
586
606
  ---
587
607
 
588
608
  #### Telegram Channel
@@ -1197,6 +1217,26 @@ if __name__ == "__main__":
1197
1217
 
1198
1218
  > **¿Probando localmente?** Usa un tunel como [ngrok](https://ngrok.com) (`ngrok http 8000`) o Cloudflare Tunnels para brindarle a Meta la URL publica HTTPS (`https://tu-dominio.ngrok-free.app/webhook`).
1199
1219
 
1220
+ > [!NOTE]
1221
+ > **Documentación interactiva en desarrollo:** Por motivos de seguridad, la documentación interactiva de FastAPI (`/docs`, `/redoc` y `/openapi.json`) está deshabilitada por defecto en producción. Para habilitar Swagger UI y los esquemas OpenAPI durante el desarrollo local o debugging, pasa `enable_docs=True` al instanciar `WhatsAppChannel`:
1222
+ > ```python
1223
+ > canal_whatsapp = WhatsAppChannel(
1224
+ > ...,
1225
+ > enable_docs=True, # Habilita /docs, /redoc y /openapi.json para desarrollo local
1226
+ > )
1227
+ > ```
1228
+
1229
+ > [!TIP]
1230
+ > **Rate Limiting y Reverse Proxy en Producción:** Para proteger los endpoints `/webhook` y `/health` contra ataques de inundación, denegación de servicio (DoS) y agotamiento de CPU por peticiones no autenticadas sin riesgo de descartar ráfagas legítimas de webhooks de Meta a nivel aplicación, ubica siempre Uvicorn detrás de un reverse proxy en producción (como Nginx con `limit_req_zone` o Cloudflare Rate Limiting):
1231
+ > ```nginx
1232
+ > limit_req_zone $binary_remote_addr zone=whatsapp_limit:10m rate=10r/s;
1233
+ > location /webhook {
1234
+ > limit_req zone=whatsapp_limit burst=20 nodelay;
1235
+ > proxy_pass http://127.0.0.1:8000;
1236
+ > proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
1237
+ > }
1238
+ > ```
1239
+
1200
1240
  ---
1201
1241
 
1202
1242
  #### Canal de Telegram
@@ -537,6 +537,26 @@ if __name__ == "__main__":
537
537
 
538
538
  > **Testing locally?** Use a tunnel like [ngrok](https://ngrok.com) (`ngrok http 8000`) or Cloudflare Tunnels to provide Meta with a public HTTPS URL (`https://your-domain.ngrok-free.app/webhook`).
539
539
 
540
+ > [!NOTE]
541
+ > **API Documentation in Development:** For security, FastAPI interactive documentation (`/docs`, `/redoc`, and `/openapi.json`) is disabled by default in production. To enable Swagger UI and OpenAPI schemas during local development or debugging, pass `enable_docs=True` when instantiating `WhatsAppChannel`:
542
+ > ```python
543
+ > channel = WhatsAppChannel(
544
+ > ...,
545
+ > enable_docs=True, # Enables /docs, /redoc, and /openapi.json for local development
546
+ > )
547
+ > ```
548
+
549
+ > [!TIP]
550
+ > **Production Rate Limiting & Reverse Proxy:** To protect `/webhook` and `/health` endpoints against flood attacks, denial of service (DoS), and CPU exhaustion from unauthenticated requests without risking dropping legitimate Meta webhook bursts at the application level, always place Uvicorn behind a reverse proxy in production (such as Nginx with `limit_req_zone` or Cloudflare Rate Limiting):
551
+ > ```nginx
552
+ > limit_req_zone $binary_remote_addr zone=whatsapp_limit:10m rate=10r/s;
553
+ > location /webhook {
554
+ > limit_req zone=whatsapp_limit burst=20 nodelay;
555
+ > proxy_pass http://127.0.0.1:8000;
556
+ > proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
557
+ > }
558
+ > ```
559
+
540
560
  ---
541
561
 
542
562
  #### Telegram Channel
@@ -1151,6 +1171,26 @@ if __name__ == "__main__":
1151
1171
 
1152
1172
  > **¿Probando localmente?** Usa un tunel como [ngrok](https://ngrok.com) (`ngrok http 8000`) o Cloudflare Tunnels para brindarle a Meta la URL publica HTTPS (`https://tu-dominio.ngrok-free.app/webhook`).
1153
1173
 
1174
+ > [!NOTE]
1175
+ > **Documentación interactiva en desarrollo:** Por motivos de seguridad, la documentación interactiva de FastAPI (`/docs`, `/redoc` y `/openapi.json`) está deshabilitada por defecto en producción. Para habilitar Swagger UI y los esquemas OpenAPI durante el desarrollo local o debugging, pasa `enable_docs=True` al instanciar `WhatsAppChannel`:
1176
+ > ```python
1177
+ > canal_whatsapp = WhatsAppChannel(
1178
+ > ...,
1179
+ > enable_docs=True, # Habilita /docs, /redoc y /openapi.json para desarrollo local
1180
+ > )
1181
+ > ```
1182
+
1183
+ > [!TIP]
1184
+ > **Rate Limiting y Reverse Proxy en Producción:** Para proteger los endpoints `/webhook` y `/health` contra ataques de inundación, denegación de servicio (DoS) y agotamiento de CPU por peticiones no autenticadas sin riesgo de descartar ráfagas legítimas de webhooks de Meta a nivel aplicación, ubica siempre Uvicorn detrás de un reverse proxy en producción (como Nginx con `limit_req_zone` o Cloudflare Rate Limiting):
1185
+ > ```nginx
1186
+ > limit_req_zone $binary_remote_addr zone=whatsapp_limit:10m rate=10r/s;
1187
+ > location /webhook {
1188
+ > limit_req zone=whatsapp_limit burst=20 nodelay;
1189
+ > proxy_pass http://127.0.0.1:8000;
1190
+ > proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
1191
+ > }
1192
+ > ```
1193
+
1154
1194
  ---
1155
1195
 
1156
1196
  #### Canal de Telegram
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "chatflow-agent"
7
- version = "0.3.0"
7
+ version = "0.3.2"
8
8
  description = "Lightweight, async multi-agent framework with native handoffs for Gemini, Gemma, Grok, OpenAI & Claude."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -34,7 +34,7 @@ from chatflow_agent.types import (
34
34
  # Alias for semantic consistency
35
35
  BaseSessionContext = SessionContext
36
36
 
37
- __version__ = "0.3.0"
37
+ __version__ = "0.3.2"
38
38
  __all__ = [
39
39
  "__version__",
40
40
  "Agent",
@@ -1,6 +1,6 @@
1
1
  """Communication channels for chatflow-agent: CLI, WhatsApp, Telegram, and Webhook."""
2
2
 
3
- from chatflow_agent.channels.base import BaseChannel, ChannelError
3
+ from chatflow_agent.channels.base import BaseChannel, ChannelError, split_message
4
4
  from chatflow_agent.channels.cli import CLIChannel
5
5
  from chatflow_agent.channels.telegram import TelegramChannel
6
6
  from chatflow_agent.channels.whatsapp import WhatsAppChannel
@@ -11,4 +11,5 @@ __all__ = [
11
11
  "ChannelError",
12
12
  "TelegramChannel",
13
13
  "WhatsAppChannel",
14
+ "split_message",
14
15
  ]
@@ -12,6 +12,42 @@ class ChannelError(ChatFlowError):
12
12
  """Raised when an operation on a channel fails or runner is detached."""
13
13
 
14
14
 
15
+ def split_message(text: str, max_len: int = 4096) -> list[str]:
16
+ """Split long outbound text into sequential chunks within max_len.
17
+
18
+ Splits progressively by paragraph (\n\n), line (\n), sentence (. ), or word ( )
19
+ to preserve formatting and readability across messaging channels.
20
+ """
21
+ if len(text) <= max_len:
22
+ return [text]
23
+
24
+ chunks: list[str] = []
25
+ remaining = text
26
+
27
+ while len(remaining) > max_len:
28
+ candidate = remaining[:max_len]
29
+ split_idx = -1
30
+
31
+ for sep in ("\n\n", "\n", ". ", " "):
32
+ idx = candidate.rfind(sep)
33
+ if idx != -1:
34
+ split_idx = idx + len(sep)
35
+ break
36
+
37
+ if split_idx <= 0:
38
+ split_idx = max_len
39
+
40
+ chunk = remaining[:split_idx].rstrip()
41
+ if chunk:
42
+ chunks.append(chunk)
43
+ remaining = remaining[split_idx:].lstrip()
44
+
45
+ if remaining:
46
+ chunks.append(remaining)
47
+
48
+ return chunks
49
+
50
+
15
51
  class BaseChannel(ABC):
16
52
  """Abstract base class for all communication channels (CLI, WhatsApp, Telegram, Webhook)."""
17
53
 
@@ -3,7 +3,7 @@
3
3
  import logging
4
4
  from typing import Any
5
5
 
6
- from chatflow_agent.channels.base import BaseChannel
6
+ from chatflow_agent.channels.base import BaseChannel, split_message
7
7
  from chatflow_agent.exceptions import DependencyError
8
8
  from chatflow_agent.types import AgentResponse
9
9
 
@@ -68,6 +68,29 @@ class TelegramChannel(BaseChannel):
68
68
  if not update.effective_chat or not update.message:
69
69
  return
70
70
 
71
+ # Restrict /reset in group and supergroup chats to creators and administrators
72
+ if getattr(update.effective_chat, "type", None) in ("group", "supergroup"):
73
+ is_admin = False
74
+ if update.effective_user and hasattr(context, "bot") and context.bot:
75
+ try:
76
+ member = await context.bot.get_chat_member(
77
+ chat_id=update.effective_chat.id,
78
+ user_id=update.effective_user.id,
79
+ )
80
+ if getattr(member, "status", None) in ("creator", "administrator"):
81
+ is_admin = True
82
+ except Exception as err:
83
+ logger.error(
84
+ f"Failed to verify chat member permissions for /reset: {err}"
85
+ )
86
+ is_admin = False
87
+
88
+ if not is_admin:
89
+ await update.message.reply_text(
90
+ "Only group admins can reset the conversation."
91
+ )
92
+ return
93
+
71
94
  chat_id = str(update.effective_chat.id)
72
95
  if self.runner:
73
96
  session = self.runner.get_session(chat_id)
@@ -103,7 +126,9 @@ class TelegramChannel(BaseChannel):
103
126
  "username": user_name,
104
127
  },
105
128
  )
106
- await update.message.reply_text(response.content)
129
+ chunks = split_message(response.content, max_len=4096)
130
+ for chunk in chunks:
131
+ await update.message.reply_text(chunk)
107
132
  except Exception as err:
108
133
  logger.error(f"Error processing Telegram message: {err}")
109
134
  await update.message.reply_text(self.fallback_message)
@@ -8,13 +8,15 @@ import json
8
8
  import logging
9
9
  from typing import Any
10
10
 
11
- from chatflow_agent.channels.base import BaseChannel
11
+ from chatflow_agent.channels.base import BaseChannel, split_message
12
12
  from chatflow_agent.core.memory import DEFAULT_DEDUP_TTL_HOURS
13
13
  from chatflow_agent.exceptions import DependencyError
14
14
  from chatflow_agent.types import AgentResponse
15
15
 
16
16
  logger = logging.getLogger("chatflow_agent.whatsapp")
17
17
 
18
+ DEFAULT_MAX_BODY_SIZE = 1 * 1024 * 1024 # 1 MB maximum allowed webhook payload size
19
+
18
20
 
19
21
  def _mask_phone(phone: str) -> str:
20
22
  """Mask phone number for GDPR/privacy compliance in logs (e.g. 5491***678)."""
@@ -23,40 +25,7 @@ def _mask_phone(phone: str) -> str:
23
25
  return f"{phone[:4]}***{phone[-3:]}"
24
26
 
25
27
 
26
- def split_message(text: str, max_len: int = 4096) -> list[str]:
27
- """Split long outbound text into sequential chunks within max_len.
28
-
29
- Splits progressively by paragraph (\n\n), line (\n), sentence (. ), or word ( )
30
- to preserve formatting and readability across messaging channels.
31
- """
32
- if len(text) <= max_len:
33
- return [text]
34
-
35
- chunks: list[str] = []
36
- remaining = text
37
-
38
- while len(remaining) > max_len:
39
- candidate = remaining[:max_len]
40
- split_idx = -1
41
-
42
- for sep in ("\n\n", "\n", ". ", " "):
43
- idx = candidate.rfind(sep)
44
- if idx != -1:
45
- split_idx = idx + len(sep)
46
- break
47
-
48
- if split_idx <= 0:
49
- split_idx = max_len
50
-
51
- chunk = remaining[:split_idx].rstrip()
52
- if chunk:
53
- chunks.append(chunk)
54
- remaining = remaining[split_idx:].lstrip()
55
-
56
- if remaining:
57
- chunks.append(remaining)
58
-
59
- return chunks
28
+ __all__ = ["WhatsAppChannel", "split_message"]
60
29
 
61
30
 
62
31
  class WhatsAppChannel(BaseChannel):
@@ -83,6 +52,8 @@ class WhatsAppChannel(BaseChannel):
83
52
  outbound_max_delay: float = 30.0,
84
53
  on_24h_window_expired: Any | None = None,
85
54
  dedup_ttl_hours: float = DEFAULT_DEDUP_TTL_HOURS,
55
+ max_body_size: int = DEFAULT_MAX_BODY_SIZE,
56
+ enable_docs: bool = False,
86
57
  ) -> None:
87
58
  super().__init__()
88
59
  # SEC-01: Fail-closed — require app_secret when signature verification is active.
@@ -112,6 +83,8 @@ class WhatsAppChannel(BaseChannel):
112
83
  self.outbound_max_delay = outbound_max_delay
113
84
  self.on_24h_window_expired = on_24h_window_expired
114
85
  self.dedup_ttl_hours = dedup_ttl_hours
86
+ self.max_body_size = max_body_size
87
+ self.enable_docs = enable_docs
115
88
 
116
89
  # Verify optional dependencies
117
90
  try:
@@ -126,7 +99,16 @@ class WhatsAppChannel(BaseChannel):
126
99
 
127
100
  self._httpx = httpx
128
101
  self._uvicorn = uvicorn
129
- self.app = FastAPI(title="ChatFlow WhatsApp Webhook Server")
102
+ # SEC-05: Disable OpenAPI schema, Swagger UI (/docs) and ReDoc (/redoc) by default in production
103
+ docs_url = "/docs" if enable_docs else None
104
+ redoc_url = "/redoc" if enable_docs else None
105
+ openapi_url = "/openapi.json" if enable_docs else None
106
+ self.app = FastAPI(
107
+ title="ChatFlow WhatsApp Webhook Server",
108
+ docs_url=docs_url,
109
+ redoc_url=redoc_url,
110
+ openapi_url=openapi_url,
111
+ )
130
112
  self._setup_routes()
131
113
 
132
114
  def _setup_routes(self) -> None:
@@ -164,8 +146,39 @@ class WhatsAppChannel(BaseChannel):
164
146
  async def handle_incoming_message(
165
147
  request: Request, background_tasks: BackgroundTasks
166
148
  ) -> Response:
167
- """Validate HMAC, deduplicate by wamid, and dispatch processing to background tasks."""
149
+ """Validate Content-Length, HMAC, deduplicate by wamid, and dispatch processing to background tasks."""
150
+ # SEC-01: Validate Content-Length before buffering body to prevent OOM DoS
151
+ content_length_header = request.headers.get("Content-Length")
152
+ if not content_length_header:
153
+ raise HTTPException(
154
+ status_code=status.HTTP_411_LENGTH_REQUIRED,
155
+ detail="Missing Content-Length header",
156
+ )
157
+
158
+ try:
159
+ content_length = int(content_length_header)
160
+ except (ValueError, TypeError) as err:
161
+ raise HTTPException(
162
+ status_code=status.HTTP_400_BAD_REQUEST,
163
+ detail="Invalid Content-Length header",
164
+ ) from err
165
+
166
+ status_413 = getattr(
167
+ status, "HTTP_413_CONTENT_TOO_LARGE", status.HTTP_413_REQUEST_ENTITY_TOO_LARGE
168
+ )
169
+
170
+ if content_length < 0 or content_length > self.max_body_size:
171
+ raise HTTPException(
172
+ status_code=status_413,
173
+ detail=f"Payload size exceeds maximum limit of {self.max_body_size} bytes",
174
+ )
175
+
168
176
  raw_body = await request.body()
177
+ if len(raw_body) > self.max_body_size:
178
+ raise HTTPException(
179
+ status_code=status_413,
180
+ detail=f"Payload body exceeds maximum limit of {self.max_body_size} bytes",
181
+ )
169
182
 
170
183
  # SEC-01: Validate X-Hub-Signature-256 when verify_signature is active
171
184
  if self.verify_signature and self.app_secret:
@@ -273,6 +286,7 @@ class WhatsAppChannel(BaseChannel):
273
286
  self, wamid: str, sender_phone: str, user_text: str | None
274
287
  ) -> None:
275
288
  """Process one WhatsApp message asynchronously with full anti-500 error shield."""
289
+ masked_phone = _mask_phone(sender_phone)
276
290
  try:
277
291
  if user_text is None:
278
292
  if self.access_token and self.phone_number_id:
@@ -293,12 +307,12 @@ class WhatsAppChannel(BaseChannel):
293
307
  reply_text = response.content
294
308
  except asyncio.TimeoutError:
295
309
  logger.error(
296
- f"LLM dispatch timed out after {self.llm_timeout_seconds}s for wamid={wamid} from {sender_phone}"
310
+ f"LLM dispatch timed out after {self.llm_timeout_seconds}s for wamid={wamid} from {masked_phone}"
297
311
  )
298
312
  reply_text = self.fallback_message
299
313
  except Exception as err:
300
314
  logger.exception(
301
- f"Error dispatching wamid={wamid} from {sender_phone}: {err}"
315
+ f"Error dispatching wamid={wamid} from {masked_phone}: {err}"
302
316
  )
303
317
  reply_text = self.fallback_message
304
318
 
@@ -314,7 +328,7 @@ class WhatsAppChannel(BaseChannel):
314
328
  )
315
329
  except Exception as fb_err:
316
330
  logger.exception(
317
- f"Failed to deliver fallback to {sender_phone}: {fb_err}"
331
+ f"Failed to deliver fallback to {masked_phone}: {fb_err}"
318
332
  )
319
333
 
320
334
  def _extract_message_and_sender(
@@ -1,7 +1,10 @@
1
1
  """Anthropic Claude engine for chatflow-agent."""
2
2
 
3
+ import asyncio
3
4
  import json
5
+ import logging
4
6
  import os
7
+ import random
5
8
  from typing import TYPE_CHECKING, Any
6
9
 
7
10
  import httpx
@@ -11,6 +14,8 @@ from chatflow_agent.core.engines.openai_compatible import _lowercase_schema_type
11
14
  from chatflow_agent.exceptions import ProviderError
12
15
  from chatflow_agent.types import Message, Role, ToolCall
13
16
 
17
+ logger = logging.getLogger("chatflow_agent.engine.anthropic")
18
+
14
19
  if TYPE_CHECKING:
15
20
  from chatflow_agent.core.agent import Agent
16
21
 
@@ -30,6 +35,9 @@ class AnthropicEngine(BaseEngine):
30
35
  self.base_url = base_url.rstrip("/")
31
36
  self.anthropic_version = anthropic_version
32
37
  self.timeout = timeout
38
+ self.max_retries = 3
39
+ self.base_delay = 1.0
40
+ self.max_delay = 10.0
33
41
  self._external_client = http_client
34
42
 
35
43
  def _format_history(self, history: list[Message]) -> list[dict[str, Any]]:
@@ -115,18 +123,55 @@ class AnthropicEngine(BaseEngine):
115
123
  }
116
124
 
117
125
  url = f"{self.base_url}/messages"
118
- client = self._external_client or httpx.AsyncClient(timeout=self.timeout)
119
-
120
- try:
121
- resp = await client.post(url, json=payload, headers=headers)
122
- if resp.status_code >= 400:
123
- raise ProviderError(f"Anthropic API error ({resp.status_code}): {resp.text}")
124
- data = resp.json()
125
- except httpx.RequestError as err:
126
- raise ProviderError(f"Anthropic connection error: {err}") from err
127
- finally:
128
- if not self._external_client:
129
- await client.aclose()
126
+
127
+ retries = 0
128
+ while True:
129
+ client = self._external_client or httpx.AsyncClient(timeout=self.timeout)
130
+ try:
131
+ resp = await client.post(url, json=payload, headers=headers)
132
+ if resp.status_code in (429, 500, 502, 503, 504, 529):
133
+ if retries < self.max_retries:
134
+ retry_after_str = resp.headers.get("Retry-After")
135
+ parsed_delay = None
136
+ if retry_after_str:
137
+ try:
138
+ parsed_delay = float(retry_after_str)
139
+ except (ValueError, TypeError):
140
+ parsed_delay = None
141
+
142
+ base = parsed_delay if parsed_delay is not None else self.base_delay * (2 ** retries)
143
+ delay = min(self.max_delay, base + random.uniform(0.0, 0.25 * base))
144
+ logger.warning(
145
+ f"Anthropic returned HTTP {resp.status_code}. Retrying in {delay:.2f}s (attempt {retries + 1}/{self.max_retries})..."
146
+ )
147
+ await asyncio.sleep(delay)
148
+ retries += 1
149
+ continue
150
+ raise ProviderError(
151
+ f"Anthropic API error ({resp.status_code}): {resp.text}"
152
+ )
153
+
154
+ if resp.status_code >= 400:
155
+ raise ProviderError(f"Anthropic API error ({resp.status_code}): {resp.text}")
156
+
157
+ data = resp.json()
158
+ break
159
+ except (httpx.ConnectError, httpx.ConnectTimeout) as net_err:
160
+ if retries < self.max_retries:
161
+ base = self.base_delay * (2 ** retries)
162
+ delay = min(self.max_delay, base + random.uniform(0.0, 0.25 * base))
163
+ logger.warning(
164
+ f"Anthropic connect error: {net_err}. Retrying in {delay:.2f}s..."
165
+ )
166
+ await asyncio.sleep(delay)
167
+ retries += 1
168
+ continue
169
+ raise ProviderError(f"Anthropic connection error: {net_err}") from net_err
170
+ except httpx.RequestError as err:
171
+ raise ProviderError(f"Anthropic connection error: {err}") from err
172
+ finally:
173
+ if not self._external_client:
174
+ await client.aclose()
130
175
 
131
176
  text_chunks: list[str] = []
132
177
  extracted_calls: list[ToolCall] = []
@@ -96,10 +96,35 @@ class OpenAIEngine(BaseEngine):
96
96
  or preset.get("base_url", "https://api.openai.com/v1")
97
97
  ).rstrip("/")
98
98
 
99
- env_key_name = preset.get("env_key", "OPENAI_API_KEY")
100
- self.api_key = api_key or os.environ.get(env_key_name) or (
101
- "ollama" if "localhost" in self.base_url or "127.0.0.1" in self.base_url else None
99
+ official_base_urls = {
100
+ p["base_url"].rstrip("/") for p in PROVIDER_PRESETS.values()
101
+ }
102
+ is_official = self.base_url in official_base_urls
103
+ is_local = any(
104
+ host in self.base_url for host in ("localhost", "127.0.0.1", "0.0.0.0")
102
105
  )
106
+
107
+ env_key_name = preset.get("env_key", "OPENAI_API_KEY")
108
+
109
+ self.api_key: str | None
110
+ if api_key is not None:
111
+ self.api_key = api_key
112
+ elif is_local:
113
+ # For local services (Ollama, LM Studio, vLLM on localhost), do not leak global cloud API keys
114
+ self.api_key = (
115
+ os.environ.get(env_key_name)
116
+ if self.provider in ("ollama", "gemma")
117
+ else None
118
+ ) or "ollama"
119
+ elif is_official:
120
+ self.api_key = os.environ.get(env_key_name)
121
+ else:
122
+ raise ValueError(
123
+ f"A custom or unofficial base_url was provided ({self.base_url!r}) without an explicit api_key. "
124
+ f"To prevent accidental leakage of your global {env_key_name} to untrusted endpoints, "
125
+ "fallback to environment API keys is disabled. "
126
+ "Please provide api_key explicitly (or pass api_key='' if no authentication is required)."
127
+ )
103
128
  self.timeout = timeout
104
129
  self.max_retries = 3
105
130
  self.base_delay = 1.0
@@ -1,6 +1,8 @@
1
1
  """Multi-agent Swarm-style execution Runner for chatflow-agent."""
2
2
 
3
3
  import asyncio
4
+ import logging
5
+ import uuid
4
6
  from typing import Any
5
7
 
6
8
  from chatflow_agent.core.agent import Agent
@@ -15,6 +17,8 @@ from chatflow_agent.types import (
15
17
  ToolResult,
16
18
  )
17
19
 
20
+ logger = logging.getLogger("chatflow_agent.runner")
21
+
18
22
 
19
23
  class Runner:
20
24
  """Orchestrates multi-agent conversation loops, tool calling, and autonomous handoffs."""
@@ -122,11 +126,16 @@ class Runner:
122
126
  try:
123
127
  raw_output = await tool_instance.execute_async(**call.args)
124
128
  except Exception as err:
129
+ trace_id = str(uuid.uuid4())
130
+ logger.error(
131
+ f"Tool execution failed [trace_id: {trace_id}] - Tool: '{call.name}' on agent '{current_agent.name}': {err}",
132
+ exc_info=True,
133
+ )
125
134
  tool_results.append(
126
135
  ToolResult(
127
136
  tool_call_id=call.id,
128
137
  name=call.name,
129
- content=f"Execution error: {err}",
138
+ content=f"Tool execution failed [trace_id: {trace_id}]. Please try a different approach or inform the user.",
130
139
  is_error=True,
131
140
  )
132
141
  )
@@ -226,9 +235,7 @@ class Runner:
226
235
  self.run_async(session_id, user_message, metadata=metadata)
227
236
  )
228
237
  except RuntimeError:
229
- return asyncio.run(
230
- self.run_async(session_id, user_message, metadata=metadata)
231
- )
238
+ return asyncio.run(self.run_async(session_id, user_message, metadata=metadata))
232
239
 
233
240
  def __repr__(self) -> str:
234
241
  return (