chatflow-agent 0.3.2__tar.gz → 0.3.3__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 (57) hide show
  1. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/.github/workflows/publish.yml +0 -3
  2. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/CHANGELOG.md +10 -0
  3. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/PKG-INFO +1 -1
  4. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/pyproject.toml +1 -1
  5. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/src/chatflow_agent/__init__.py +1 -1
  6. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/src/chatflow_agent/core/engines/openai_compatible.py +24 -3
  7. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/tests/test_multi_engine.py +96 -0
  8. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/.github/workflows/test.yml +0 -0
  9. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/.gitignore +0 -0
  10. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/LICENSE +0 -0
  11. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/README.md +0 -0
  12. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/examples/cli_multiagent.py +0 -0
  13. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/examples/local_gemma_cli.py +0 -0
  14. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/examples/multi_provider_swarm.py +0 -0
  15. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/examples/telegram_persistent_bot.py +0 -0
  16. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/examples/telegram_triage.py +0 -0
  17. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/examples/webhook_api_service.py +0 -0
  18. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/examples/whatsapp_persistent_bot.py +0 -0
  19. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/examples/whatsapp_service.py +0 -0
  20. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/src/chatflow_agent/channels/__init__.py +0 -0
  21. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/src/chatflow_agent/channels/base.py +0 -0
  22. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/src/chatflow_agent/channels/cli.py +0 -0
  23. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/src/chatflow_agent/channels/telegram.py +0 -0
  24. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/src/chatflow_agent/channels/whatsapp.py +0 -0
  25. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/src/chatflow_agent/core/__init__.py +0 -0
  26. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/src/chatflow_agent/core/agent.py +0 -0
  27. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/src/chatflow_agent/core/engine.py +0 -0
  28. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/src/chatflow_agent/core/engines/__init__.py +0 -0
  29. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/src/chatflow_agent/core/engines/anthropic.py +0 -0
  30. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/src/chatflow_agent/core/engines/base.py +0 -0
  31. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/src/chatflow_agent/core/engines/factory.py +0 -0
  32. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/src/chatflow_agent/core/engines/gemini.py +0 -0
  33. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/src/chatflow_agent/core/memory.py +0 -0
  34. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/src/chatflow_agent/core/runner.py +0 -0
  35. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/src/chatflow_agent/core/tools.py +0 -0
  36. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/src/chatflow_agent/exceptions.py +0 -0
  37. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/src/chatflow_agent/memory.py +0 -0
  38. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/src/chatflow_agent/py.typed +0 -0
  39. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/src/chatflow_agent/types.py +0 -0
  40. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/tests/__init__.py +0 -0
  41. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/tests/test_agent.py +0 -0
  42. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/tests/test_cli_channel.py +0 -0
  43. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/tests/test_memory.py +0 -0
  44. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/tests/test_memory_integrity.py +0 -0
  45. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/tests/test_memory_sqlite.py +0 -0
  46. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/tests/test_resilience.py +0 -0
  47. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/tests/test_runner.py +0 -0
  48. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/tests/test_runner_rollback.py +0 -0
  49. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/tests/test_runner_security.py +0 -0
  50. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/tests/test_telegram.py +0 -0
  51. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/tests/test_tools.py +0 -0
  52. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/tests/test_types.py +0 -0
  53. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/tests/test_whatsapp.py +0 -0
  54. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/tests/test_whatsapp_background.py +0 -0
  55. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/tests/test_whatsapp_resilience_outbound.py +0 -0
  56. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/tests/test_whatsapp_security.py +0 -0
  57. {chatflow_agent-0.3.2 → chatflow_agent-0.3.3}/tests/test_whatsapp_split.py +0 -0
@@ -3,9 +3,6 @@ name: Publish to PyPI
3
3
  on:
4
4
  release:
5
5
  types: [published]
6
- push:
7
- tags:
8
- - "v*"
9
6
 
10
7
  jobs:
11
8
  build-and-publish:
@@ -14,6 +14,11 @@ Versioning / Versionado: [Semantic Versioning](https://semver.org/spec/v2.0.0.ht
14
14
 
15
15
  ## [Unreleased]
16
16
 
17
+ ## [0.3.3] - 2026-10-19
18
+
19
+ ### Security
20
+ - **Strict Local Endpoint Hostname Validation:** `OpenAIEngine` now validates `base_url` structure, HTTP/HTTPS scheme, port range, and parsed hostname via `urllib.parse.urlsplit` when identifying local endpoints (`localhost`, `127.0.0.1`, `::1`, and local bind compatibility address `0.0.0.0`), replacing substring matching (Issue #74).
21
+
17
22
  ## [0.3.2] - 2026-09-29
18
23
 
19
24
  ### Security
@@ -69,6 +74,11 @@ If you are upgrading from v0.2.0:
69
74
 
70
75
  ## [Unreleased]
71
76
 
77
+ ## [0.3.3] - 2026-10-19
78
+
79
+ ### Seguridad
80
+ - **Validación Estricta de Hostname en Endpoints Locales:** `OpenAIEngine` ahora valida la estructura de `base_url`, el esquema HTTP/HTTPS, el rango de puerto y el hostname real mediante `urllib.parse.urlsplit` al identificar endpoints locales (`localhost`, `127.0.0.1`, `::1` y la dirección de compatibilidad local `0.0.0.0`), reemplazando la coincidencia por subcadenas (Issue #74).
81
+
72
82
  ## [0.3.2] - 2026-09-29
73
83
 
74
84
  ### Seguridad
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: chatflow-agent
3
- Version: 0.3.2
3
+ Version: 0.3.3
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "chatflow-agent"
7
- version = "0.3.2"
7
+ version = "0.3.3"
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.2"
37
+ __version__ = "0.3.3"
38
38
  __all__ = [
39
39
  "__version__",
40
40
  "Agent",
@@ -10,6 +10,7 @@ import json
10
10
  import logging
11
11
  import os
12
12
  from typing import TYPE_CHECKING, Any
13
+ from urllib.parse import urlsplit
13
14
 
14
15
  import httpx
15
16
 
@@ -22,6 +23,28 @@ logger = logging.getLogger("chatflow_agent.engine.openai")
22
23
  if TYPE_CHECKING:
23
24
  from chatflow_agent.core.agent import Agent
24
25
 
26
+ _LOOPBACK_HOSTS: frozenset[str] = frozenset({"localhost", "127.0.0.1", "::1"})
27
+ # Explicit compatibility exception for local dev servers bound to 0.0.0.0 (not a loopback address)
28
+ _COMPAT_LOCAL_BIND_HOSTS: frozenset[str] = frozenset({"0.0.0.0"})
29
+
30
+
31
+ def _is_local_base_url(base_url: str) -> bool:
32
+ """Return True only when base_url is a well-formed HTTP(S) URL targeting a local host."""
33
+ try:
34
+ parsed = urlsplit(base_url)
35
+ _ = parsed.port
36
+ except ValueError:
37
+ return False
38
+
39
+ if parsed.scheme.lower() not in ("http", "https") or not parsed.netloc:
40
+ return False
41
+ if parsed.username is not None or parsed.password is not None:
42
+ return False
43
+
44
+ hostname = (parsed.hostname or "").lower().rstrip(".")
45
+ return hostname in _LOOPBACK_HOSTS or hostname in _COMPAT_LOCAL_BIND_HOSTS
46
+
47
+
25
48
  # Preset configurations for common OpenAI-compatible providers
26
49
  PROVIDER_PRESETS: dict[str, dict[str, str]] = {
27
50
  "openai": {
@@ -100,9 +123,7 @@ class OpenAIEngine(BaseEngine):
100
123
  p["base_url"].rstrip("/") for p in PROVIDER_PRESETS.values()
101
124
  }
102
125
  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")
105
- )
126
+ is_local = _is_local_base_url(self.base_url)
106
127
 
107
128
  env_key_name = preset.get("env_key", "OPENAI_API_KEY")
108
129
 
@@ -279,6 +279,102 @@ def test_custom_base_url_rejects_fallback_to_global_api_key(monkeypatch: pytest.
279
279
  assert engine_official.api_key == "sk-secret-prod-key-12345"
280
280
 
281
281
 
282
+ @pytest.mark.parametrize(
283
+ "spoofed_or_malformed_url",
284
+ [
285
+ "https://localhost.untrusted.example.com/v1",
286
+ "https://sub.localhost.example.com/v1",
287
+ "https://my-localhost.example.com/v1",
288
+ "https://untrusted.example.com/localhost/v1",
289
+ "https://untrusted.example.com/v1?origin=localhost",
290
+ "https://localhost@untrusted.example.com/v1",
291
+ "http://user:pass@localhost:11434/v1",
292
+ "https://127.0.0.1.untrusted.example.com/v1",
293
+ "https://untrusted.example.com/127.0.0.1/v1",
294
+ "https://0.0.0.0.untrusted.example.com/v1",
295
+ "ftp://localhost:11434/v1",
296
+ "localhost:11434/v1",
297
+ "http:///localhost/v1",
298
+ "http://[::1:11434/v1",
299
+ "http://localhost:badport/v1",
300
+ "http://localhost:99999/v1",
301
+ ],
302
+ )
303
+ def test_spoofed_or_malformed_local_base_url_is_rejected(
304
+ monkeypatch: pytest.MonkeyPatch, spoofed_or_malformed_url: str
305
+ ) -> None:
306
+ """External or malformed URLs containing local substrings must not bypass SEC-07 or leak env keys."""
307
+ monkeypatch.setenv("OPENAI_API_KEY", "sk-secret-prod-key")
308
+ monkeypatch.setenv("OLLAMA_API_KEY", "ollama-secret-token")
309
+
310
+ for provider in ("openai", "ollama", "gemma"):
311
+ with pytest.raises(ValueError, match="A custom or unofficial base_url was provided"):
312
+ OpenAIEngine(provider=provider, base_url=spoofed_or_malformed_url)
313
+
314
+
315
+ @pytest.mark.parametrize(
316
+ "spoofed_url",
317
+ [
318
+ "https://localhost.untrusted.example.com/v1",
319
+ "https://untrusted.example.com/localhost/v1",
320
+ "https://127.0.0.1.untrusted.example.com/v1",
321
+ "https://0.0.0.0.untrusted.example.com/v1",
322
+ "https://localhost@untrusted.example.com/v1",
323
+ ],
324
+ )
325
+ def test_spoofed_local_base_url_from_env_vars_is_rejected(
326
+ monkeypatch: pytest.MonkeyPatch, spoofed_url: str
327
+ ) -> None:
328
+ """Spoofed URLs provided via OPENAI_BASE_URL or OLLAMA_BASE_URL without explicit base_url must be rejected."""
329
+ monkeypatch.setenv("OPENAI_API_KEY", "sk-secret-prod-key")
330
+ monkeypatch.setenv("OLLAMA_API_KEY", "ollama-secret-token")
331
+
332
+ monkeypatch.setenv("OPENAI_BASE_URL", spoofed_url)
333
+ with pytest.raises(ValueError, match="A custom or unofficial base_url was provided"):
334
+ OpenAIEngine(provider="openai")
335
+
336
+ monkeypatch.setenv("OLLAMA_BASE_URL", spoofed_url)
337
+ with pytest.raises(ValueError, match="A custom or unofficial base_url was provided"):
338
+ OpenAIEngine(provider="ollama")
339
+
340
+
341
+ @pytest.mark.parametrize(
342
+ "local_url",
343
+ [
344
+ "http://localhost:11434/v1",
345
+ "http://LOCALHOST:11434/v1/",
346
+ "http://localhost.:11434/v1",
347
+ "https://localhost:8443/v1",
348
+ "http://127.0.0.1:11434/v1",
349
+ "http://[::1]:11434/v1",
350
+ "http://0.0.0.0:8000/v1",
351
+ ],
352
+ )
353
+ def test_valid_local_base_urls_are_accepted_and_protect_cloud_keys(
354
+ monkeypatch: pytest.MonkeyPatch, local_url: str
355
+ ) -> None:
356
+ """Valid loopback (localhost, 127.0.0.1, ::1) and compatibility 0.0.0.0 URLs work without leaking cloud keys."""
357
+ monkeypatch.setenv("OPENAI_API_KEY", "sk-secret-prod-key")
358
+ monkeypatch.delenv("OLLAMA_API_KEY", raising=False)
359
+
360
+ # provider='openai' pointed at local server must NOT leak OPENAI_API_KEY
361
+ engine_openai = OpenAIEngine(provider="openai", base_url=local_url)
362
+ assert engine_openai.api_key == "ollama"
363
+
364
+ # provider='ollama' / 'gemma' without OLLAMA_API_KEY defaults to 'ollama'
365
+ engine_ollama = OpenAIEngine(provider="ollama", base_url=local_url)
366
+ assert engine_ollama.api_key == "ollama"
367
+ engine_gemma = OpenAIEngine(provider="gemma", base_url=local_url)
368
+ assert engine_gemma.api_key == "ollama"
369
+
370
+ # provider='ollama' / 'gemma' with OLLAMA_API_KEY honors it on genuine local URLs
371
+ monkeypatch.setenv("OLLAMA_API_KEY", "local-ollama-token")
372
+ engine_ollama_auth = OpenAIEngine(provider="ollama", base_url=local_url)
373
+ assert engine_ollama_auth.api_key == "local-ollama-token"
374
+ engine_gemma_auth = OpenAIEngine(provider="gemma", base_url=local_url)
375
+ assert engine_gemma_auth.api_key == "local-ollama-token"
376
+
377
+
282
378
  @pytest.mark.asyncio
283
379
  async def test_anthropic_retry_on_529_overloaded() -> None:
284
380
  """Test AnthropicEngine retries on HTTP 529 Overloaded and succeeds."""
File without changes
File without changes