From 3237d44d17333b30122be1cbb0cc2f3522ce6ea1 Mon Sep 17 00:00:00 2001 From: Volpsmx Date: Wed, 12 Aug 2026 01:40:55 -0600 Subject: [PATCH 01/12] Implement tests for LexMexTool Add tests for LexMexTool to ensure API key handling and functionality. --- .../tests/tools/test_lexmex_tool.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 lib/crewai-tools/src/lib/crewai-tools/tests/tools/test_lexmex_tool.py diff --git a/lib/crewai-tools/src/lib/crewai-tools/tests/tools/test_lexmex_tool.py b/lib/crewai-tools/src/lib/crewai-tools/tests/tools/test_lexmex_tool.py new file mode 100644 index 0000000000..a1b2031f72 --- /dev/null +++ b/lib/crewai-tools/src/lib/crewai-tools/tests/tools/test_lexmex_tool.py @@ -0,0 +1,36 @@ +import pytest + +from crewai_tools.tools.lexmex_tool.lexmex_tool import LexMexTool + + +def test_requires_api_key(monkeypatch): + monkeypatch.delenv("LEXMEX_API_KEY", raising=False) + tool = LexMexTool() + with pytest.raises(ValueError): + tool._resolved_key() + + +def test_happy_path(monkeypatch): + tool = LexMexTool(api_key="lmx_live_test") + + class FakeResponse: + status_code = 200 + + def raise_for_status(self): + pass + + def json(self): + return { + "respuesta": "El despido justificado requiere...", + "fuentes": [{"cita": "LFT, art. 47"}], + "confianza": "alta", + } + + monkeypatch.setattr( + "crewai_tools.tools.lexmex_tool.lexmex_tool.requests.post", + lambda *a, **kw: FakeResponse(), + ) + + resultado = tool.run(pregunta="¿Causales de despido justificado?") + assert "despido justificado" in resultado + assert "LFT, art. 47" in resultado From 3f926f031907413d77366cebde31e55071fc90ed Mon Sep 17 00:00:00 2001 From: Volpsmx Date: Wed, 12 Aug 2026 01:52:26 -0600 Subject: [PATCH 02/12] Add LexMexTool to imports and tool list --- lib/crewai-tools/src/crewai_tools/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/crewai-tools/src/crewai_tools/__init__.py b/lib/crewai-tools/src/crewai_tools/__init__.py index bb8a946c8a..f25c9bcc34 100644 --- a/lib/crewai-tools/src/crewai_tools/__init__.py +++ b/lib/crewai-tools/src/crewai_tools/__init__.py @@ -148,6 +148,7 @@ QdrantVectorSearchTool, ) from crewai_tools.tools.rag.rag_tool import RagTool +from crewai_tools.tools.lexmex_tool.lexmex_tool import LexMexTool from crewai_tools.tools.scrape_element_from_website.scrape_element_from_website import ( ScrapeElementFromWebsiteTool, ) @@ -280,6 +281,7 @@ "JSONSearchTool", "JinaScrapeWebsiteTool", "LinkupSearchTool", + "LexMexTool", "LlamaIndexTool", "MCPServerAdapter", "MDXSearchTool", From 1d1b62ae2fd022f3e4f91f716ac5d6fd92647a0e Mon Sep 17 00:00:00 2001 From: Volpsmx Date: Wed, 12 Aug 2026 02:01:18 -0600 Subject: [PATCH 03/12] Add LexMexTool for querying Mexican federal laws This file implements the LexMexTool for querying Mexican federal laws using the LEX-MEX API. It includes input validation, error handling, and response formatting. --- .../tools/lexmex_tool/lexmex_tool.py | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 lib/crewai-tools/src/crewai_tools/tools/lexmex_tool/lexmex_tool.py diff --git a/lib/crewai-tools/src/crewai_tools/tools/lexmex_tool/lexmex_tool.py b/lib/crewai-tools/src/crewai_tools/tools/lexmex_tool/lexmex_tool.py new file mode 100644 index 0000000000..c6ca670e31 --- /dev/null +++ b/lib/crewai-tools/src/crewai_tools/tools/lexmex_tool/lexmex_tool.py @@ -0,0 +1,120 @@ +""" +lexmex_tool.py — LEX-MEX v1.0 +═══════════════════════════════════════════════════════════════════════ +Tool de CrewAI para consultar LEX-MEX (asesor jurídico de leyes +federales mexicanas) desde cualquier Agent de un Crew. + +Requiere una API key real de LEX-MEX (plan VIP o pay-as-you-go), +obtenida por un humano en https://lex-mex.xyz tras registrarse +(Google OAuth). Esta tool NO emite ni gestiona la key, solo la usa. + +Uso: + from crewai_tools.tools.lexmex_tool.lexmex_tool import LexMexTool + from crewai import Agent + + abogado = Agent( + role="Asesor legal", + goal="Responder dudas de derecho federal mexicano con cita exacta", + backstory="Experto en legislación federal mexicana.", + tools=[LexMexTool(api_key="lmx_live_...")], + ) + + # o vía variable de entorno LEXMEX_API_KEY + import os + os.environ["LEXMEX_API_KEY"] = "lmx_live_..." + tool = LexMexTool() +""" + +from __future__ import annotations + +import os +from typing import Optional, Type + +import requests +from pydantic import BaseModel, Field + +try: + from crewai.tools import BaseTool +except ImportError as e: # pragma: no cover + raise ImportError( + "LexMexTool requiere crewai. Instala con: pip install crewai" + ) from e + + +LEXMEX_API_BASE = "https://lex-mex.xyz" + + +class LexMexInput(BaseModel): + pregunta: str = Field( + ..., + description=( + "Consulta jurídica en español sobre derecho federal mexicano " + "(316 leyes federales indexadas, sincronizadas con " + "diputados.gob.mx). Ej: '¿Cuáles son las causales de despido " + "justificado según la LFT?'" + ), + min_length=1, + max_length=2000, + ) + + +class LexMexTool(BaseTool): + """Consulta leyes federales mexicanas con respuestas citadas al DOF. + + Motor RAG+LLM real de lex-mex.xyz — no alucina artículos, cita la + ley y el artículo exacto de donde saca cada respuesta. Requiere + API key de plan VIP o de créditos pay-as-you-go. + """ + + name: str = "Consulta legal LEX-MEX" + description: str = ( + "Útil para responder preguntas sobre leyes federales mexicanas " + "(laboral, civil, fiscal, penal, mercantil, etc.). Devuelve la " + "respuesta junto con las fuentes legales citadas (ley y " + "artículo) y un nivel de confianza. Input: una pregunta legal " + "en español, texto plano." + ) + args_schema: Type[BaseModel] = LexMexInput + + api_key: Optional[str] = None + api_base: str = LEXMEX_API_BASE + timeout: int = 30 + + def _resolved_key(self) -> str: + key = self.api_key or os.getenv("LEXMEX_API_KEY") + if not key: + raise ValueError( + "Falta la API key de LEX-MEX. Pásala como LexMexTool(api_key=...) " + "o define la variable de entorno LEXMEX_API_KEY. Genera una en " + "https://lex-mex.xyz tras registrarte (plan VIP o pay-as-you-go)." + ) + return key + + def _run(self, pregunta: str) -> str: + resp = requests.post( + f"{self.api_base}/api/v1/consulta", + json={"pregunta": pregunta}, + headers={"X-API-Key": self._resolved_key()}, + timeout=self.timeout, + ) + + if resp.status_code == 401: + return "Error: API key de LEX-MEX inválida o revocada." + if resp.status_code == 402: + return "Error: sin saldo/plan suficiente en LEX-MEX para esta consulta." + if resp.status_code == 429: + return "Error: límite diario de consultas de LEX-MEX alcanzado." + resp.raise_for_status() + data = resp.json() + + fuentes = data.get("fuentes") or [] + fuentes_txt = "; ".join( + f.get("cita", str(f)) if isinstance(f, dict) else str(f) + for f in fuentes + ) or "sin fuentes citadas" + + return ( + f"{data.get('respuesta', '')}\n\n" + f"Fuentes: {fuentes_txt}\n" + f"Confianza: {data.get('confianza', 'n/d')}" + ) From b66d9075b6b02d52a97da96b6de5705c029e36ac Mon Sep 17 00:00:00 2001 From: Volpsmx Date: Wed, 12 Aug 2026 02:02:44 -0600 Subject: [PATCH 04/12] Create README.md for LexMexTool Added README for LexMexTool with installation, usage, and common errors. --- .../crewai_tools/tools/lexmex_tool/README.md | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 lib/crewai-tools/src/crewai_tools/tools/lexmex_tool/README.md diff --git a/lib/crewai-tools/src/crewai_tools/tools/lexmex_tool/README.md b/lib/crewai-tools/src/crewai_tools/tools/lexmex_tool/README.md new file mode 100644 index 0000000000..fb1b8f98e9 --- /dev/null +++ b/lib/crewai-tools/src/crewai_tools/tools/lexmex_tool/README.md @@ -0,0 +1,37 @@ +# LexMexTool + +Consulta [LEX-MEX](https://lex-mex.xyz) — asesor jurídico con IA sobre +las 316 leyes federales mexicanas vigentes, sincronizadas con +`diputados.gob.mx`. Cada respuesta cita la ley y el artículo exacto +(sin alucinar fuentes). + +## Instalación + +Esta tool solo necesita `requests` y `pydantic`, ambos ya son +dependencias de `crewai-tools`. No requiere paquete extra. + +## Variables de entorno + +| Variable | Requerida | Descripción | +| --- | --- | --- | +| `LEXMEX_API_KEY` | Sí (o pásala como `api_key=` al instanciar) | API key de LEX-MEX. Se genera en `POST /api/v1/keys` tras registrarte en https://lex-mex.xyz (Google OAuth). Plan VIP ilimitado o pay-as-you-go por créditos. | + +## Uso + +```python +from crewai import Agent +from crewai_tools import LexMexTool + +abogado = Agent( + role="Asesor legal", + goal="Responder dudas de derecho federal mexicano con cita exacta", + backstory="Experto en legislación federal mexicana.", + tools=[LexMexTool()], # toma LEXMEX_API_KEY del entorno +) +``` + +## Errores comunes + +- `401` → API key inválida o revocada. +- `402` → sin saldo/plan suficiente para la consulta. +- `429` → límite diario de consultas alcanzado. From 37371bc94b5dc292c8a134054b590c5524c5b851 Mon Sep 17 00:00:00 2001 From: Volpsmx Date: Wed, 12 Aug 2026 02:03:41 -0600 Subject: [PATCH 05/12] Initialize lexmex_tool module with LexMexTool import --- .../src/crewai_tools/tools/lexmex_tool/__init__.py | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 lib/crewai-tools/src/crewai_tools/tools/lexmex_tool/__init__.py diff --git a/lib/crewai-tools/src/crewai_tools/tools/lexmex_tool/__init__.py b/lib/crewai-tools/src/crewai_tools/tools/lexmex_tool/__init__.py new file mode 100644 index 0000000000..9442531066 --- /dev/null +++ b/lib/crewai-tools/src/crewai_tools/tools/lexmex_tool/__init__.py @@ -0,0 +1,3 @@ +from crewai_tools.tools.lexmex_tool.lexmex_tool import LexMexTool + +__all__ = ["LexMexTool"] From fa0c363b8f1dbc6dee9e5e9927f141b97f800176 Mon Sep 17 00:00:00 2001 From: Volpsmx Date: Wed, 19 Aug 2026 23:21:42 -0600 Subject: [PATCH 06/12] Bump version to 1.1 with security enhancements Updated version to 1.1 with security improvements, including exclusion of api_key from serialization and fixed api_base. Added X-LexMex-Client header for backend identification. --- .../tools/lexmex_tool/lexmex_tool.py | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/lib/crewai-tools/src/crewai_tools/tools/lexmex_tool/lexmex_tool.py b/lib/crewai-tools/src/crewai_tools/tools/lexmex_tool/lexmex_tool.py index c6ca670e31..abb6fc9867 100644 --- a/lib/crewai-tools/src/crewai_tools/tools/lexmex_tool/lexmex_tool.py +++ b/lib/crewai-tools/src/crewai_tools/tools/lexmex_tool/lexmex_tool.py @@ -1,5 +1,5 @@ """ -lexmex_tool.py — LEX-MEX v1.0 +lexmex_tool.py — LEX-MEX v1.1 ═══════════════════════════════════════════════════════════════════════ Tool de CrewAI para consultar LEX-MEX (asesor jurídico de leyes federales mexicanas) desde cualquier Agent de un Crew. @@ -23,6 +23,17 @@ import os os.environ["LEXMEX_API_KEY"] = "lmx_live_..." tool = LexMexTool() + +─── Changelog v1.1 ──────────────────────────────────────────────────── +- Seguridad: api_key ahora se excluye de la serialización del modelo + (Field(exclude=True, repr=False)) para que nunca quede persistida en + logs ni en estados de Crew guardados. +- Seguridad: se eliminó el campo configurable `api_base`; el host + autenticado ahora es siempre la constante fija LEXMEX_API_BASE, para + que ningún caller pueda redirigir la API key a un host arbitrario + (hallazgo de CodeRabbit: API-key exfiltration risk). +- Se agregó el header X-LexMex-Client para que el backend de LexMex + pueda distinguir tráfico proveniente de esta tool. """ from __future__ import annotations @@ -42,6 +53,7 @@ LEXMEX_API_BASE = "https://lex-mex.xyz" +LEXMEX_CLIENT_ID = "crewai-lexmex-tool/1.1.0" class LexMexInput(BaseModel): @@ -76,8 +88,9 @@ class LexMexTool(BaseTool): ) args_schema: Type[BaseModel] = LexMexInput - api_key: Optional[str] = None - api_base: str = LEXMEX_API_BASE + # Se excluye de la serialización (model_dump/repr) para que la key + # nunca quede persistida en logs, estados de Crew guardados, etc. + api_key: Optional[str] = Field(default=None, exclude=True, repr=False) timeout: int = 30 def _resolved_key(self) -> str: @@ -92,9 +105,12 @@ def _resolved_key(self) -> str: def _run(self, pregunta: str) -> str: resp = requests.post( - f"{self.api_base}/api/v1/consulta", + f"{LEXMEX_API_BASE}/api/v1/consulta", json={"pregunta": pregunta}, - headers={"X-API-Key": self._resolved_key()}, + headers={ + "X-API-Key": self._resolved_key(), + "X-LexMex-Client": LEXMEX_CLIENT_ID, + }, timeout=self.timeout, ) From 4c09fd130d547ee21bf71dcc6cd9b3c800b0f420 Mon Sep 17 00:00:00 2001 From: Volpsmx Date: Wed, 19 Aug 2026 23:24:29 -0600 Subject: [PATCH 07/12] Enhance tests for LexMexTool API key behavior Added tests for LexMexTool to validate API key handling and response scenarios. --- .../tests/tools/test_lexmex_tool.py | 86 ++++++++++++++++++- 1 file changed, 85 insertions(+), 1 deletion(-) diff --git a/lib/crewai-tools/src/lib/crewai-tools/tests/tools/test_lexmex_tool.py b/lib/crewai-tools/src/lib/crewai-tools/tests/tools/test_lexmex_tool.py index a1b2031f72..bfde9ca5ef 100644 --- a/lib/crewai-tools/src/lib/crewai-tools/tests/tools/test_lexmex_tool.py +++ b/lib/crewai-tools/src/lib/crewai-tools/tests/tools/test_lexmex_tool.py @@ -10,8 +10,32 @@ def test_requires_api_key(monkeypatch): tool._resolved_key() +def test_reads_key_from_env(monkeypatch): + monkeypatch.setenv("LEXMEX_API_KEY", "lmx_live_env") + tool = LexMexTool() + assert tool._resolved_key() == "lmx_live_env" + + +def test_explicit_key_overrides_env(monkeypatch): + monkeypatch.setenv("LEXMEX_API_KEY", "lmx_live_env") + tool = LexMexTool(api_key="lmx_live_explicit") + assert tool._resolved_key() == "lmx_live_explicit" + + +def test_api_key_excluded_from_serialization(): + """CodeRabbit: la key nunca debe aparecer en model_dump/repr.""" + tool = LexMexTool(api_key="lmx_live_secret") + dumped = tool.model_dump() + assert "api_key" not in dumped + assert "lmx_live_secret" not in repr(tool) + + def test_happy_path(monkeypatch): + """CodeRabbit: se verifica el contrato real de la llamada saliente + (URL fija, header X-API-Key, header X-LexMex-Client), no solo el + resultado final formateado.""" tool = LexMexTool(api_key="lmx_live_test") + captured = {} class FakeResponse: status_code = 200 @@ -26,11 +50,71 @@ def json(self): "confianza": "alta", } + def fake_post(url, json, headers, timeout): + captured["url"] = url + captured["json"] = json + captured["headers"] = headers + captured["timeout"] = timeout + return FakeResponse() + monkeypatch.setattr( "crewai_tools.tools.lexmex_tool.lexmex_tool.requests.post", - lambda *a, **kw: FakeResponse(), + fake_post, ) resultado = tool.run(pregunta="¿Causales de despido justificado?") + + assert captured["url"] == "https://lex-mex.xyz/api/v1/consulta" + assert captured["json"] == {"pregunta": "¿Causales de despido justificado?"} + assert captured["headers"]["X-API-Key"] == "lmx_live_test" + assert captured["headers"]["X-LexMex-Client"] == "crewai-lexmex-tool/1.1.0" + assert captured["timeout"] == 30 + assert "despido justificado" in resultado assert "LFT, art. 47" in resultado + assert "alta" in resultado + + +def test_unauthorized(monkeypatch): + tool = LexMexTool(api_key="lmx_live_bad") + + class FakeResponse: + status_code = 401 + + monkeypatch.setattr( + "crewai_tools.tools.lexmex_tool.lexmex_tool.requests.post", + lambda *a, **kw: FakeResponse(), + ) + + resultado = tool.run(pregunta="cualquier pregunta") + assert "inválida" in resultado.lower() + + +def test_insufficient_balance(monkeypatch): + tool = LexMexTool(api_key="lmx_live_test") + + class FakeResponse: + status_code = 402 + + monkeypatch.setattr( + "crewai_tools.tools.lexmex_tool.lexmex_tool.requests.post", + lambda *a, **kw: FakeResponse(), + ) + + resultado = tool.run(pregunta="cualquier pregunta") + assert "saldo" in resultado.lower() + + +def test_rate_limited(monkeypatch): + tool = LexMexTool(api_key="lmx_live_test") + + class FakeResponse: + status_code = 429 + + monkeypatch.setattr( + "crewai_tools.tools.lexmex_tool.lexmex_tool.requests.post", + lambda *a, **kw: FakeResponse(), + ) + + resultado = tool.run(pregunta="cualquier pregunta") + assert "límite" in resultado.lower() From bbedd8549c0307e4a453941f9865c90d608acc25 Mon Sep 17 00:00:00 2001 From: Volpsmx Date: Thu, 20 Aug 2026 13:47:10 -0600 Subject: [PATCH 08/12] Refactor timeout attribute with Field validation Updated the timeout attribute to use Field with validation and description. --- .../src/crewai_tools/tools/lexmex_tool/lexmex_tool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/crewai-tools/src/crewai_tools/tools/lexmex_tool/lexmex_tool.py b/lib/crewai-tools/src/crewai_tools/tools/lexmex_tool/lexmex_tool.py index abb6fc9867..482bbbc272 100644 --- a/lib/crewai-tools/src/crewai_tools/tools/lexmex_tool/lexmex_tool.py +++ b/lib/crewai-tools/src/crewai_tools/tools/lexmex_tool/lexmex_tool.py @@ -91,7 +91,7 @@ class LexMexTool(BaseTool): # Se excluye de la serialización (model_dump/repr) para que la key # nunca quede persistida en logs, estados de Crew guardados, etc. api_key: Optional[str] = Field(default=None, exclude=True, repr=False) - timeout: int = 30 + timeout: int = Field(default=30, gt=0, description="Request timeout in seconds.") def _resolved_key(self) -> str: key = self.api_key or os.getenv("LEXMEX_API_KEY") From f5de29daea4709aa40bd90bfb416f0fca7ee8f61 Mon Sep 17 00:00:00 2001 From: Volpsmx Date: Thu, 20 Aug 2026 14:15:33 -0600 Subject: [PATCH 09/12] Mejorar seguridad y robustez en LexMexTool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Se mejoró la seguridad y robustez de la herramienta LexMex. Se agregó validación de respuesta JSON y se ajustó la documentación para reflejar la naturaleza de la información legal proporcionada. --- .../tools/lexmex_tool/lexmex_tool.py | 67 +++++++++++++++---- 1 file changed, 54 insertions(+), 13 deletions(-) diff --git a/lib/crewai-tools/src/crewai_tools/tools/lexmex_tool/lexmex_tool.py b/lib/crewai-tools/src/crewai_tools/tools/lexmex_tool/lexmex_tool.py index 482bbbc272..d8e49db4dd 100644 --- a/lib/crewai-tools/src/crewai_tools/tools/lexmex_tool/lexmex_tool.py +++ b/lib/crewai-tools/src/crewai_tools/tools/lexmex_tool/lexmex_tool.py @@ -34,6 +34,20 @@ (hallazgo de CodeRabbit: API-key exfiltration risk). - Se agregó el header X-LexMex-Client para que el backend de LexMex pueda distinguir tráfico proveniente de esta tool. + +─── Changelog v1.2 ──────────────────────────────────────────────────── +- Seguridad: se agregó `allow_redirects=False` a la llamada HTTP y se + rechaza cualquier respuesta 3xx, para que la X-API-Key nunca se + reenvíe a un host distinto de lex-mex.xyz vía redirect (hallazgo de + CodeRabbit: `requests` no limpia headers custom en redirects). +- Robustez: se valida que la respuesta JSON sea un objeto, que + `respuesta` sea un string no vacío y que `fuentes` sea una lista + antes de formatear la salida, para no devolver un resultado + aparentemente exitoso ante un JSON malformado o incompleto. +- Se suavizaron las afirmaciones absolutas de precisión legal en el + docstring/description ("no alucina", "cita exacta") por lenguaje que + describe la salida como información legal general y recomienda + verificarla con la fuente oficial o un abogado. """ from __future__ import annotations @@ -71,20 +85,27 @@ class LexMexInput(BaseModel): class LexMexTool(BaseTool): - """Consulta leyes federales mexicanas con respuestas citadas al DOF. - - Motor RAG+LLM real de lex-mex.xyz — no alucina artículos, cita la - ley y el artículo exacto de donde saca cada respuesta. Requiere - API key de plan VIP o de créditos pay-as-you-go. + """Consulta leyes federales mexicanas con respuestas que citan fuente. + + Motor RAG+LLM de lex-mex.xyz — devuelve información legal general + sobre derecho federal mexicano junto con la ley y el artículo en los + que se basó la respuesta. No sustituye asesoría legal profesional: + el usuario debe verificar la información con la fuente oficial + vigente (DOF / diputados.gob.mx) o con un abogado antes de tomar + decisiones legales. Requiere API key de plan VIP o de créditos + pay-as-you-go. """ name: str = "Consulta legal LEX-MEX" description: str = ( - "Útil para responder preguntas sobre leyes federales mexicanas " - "(laboral, civil, fiscal, penal, mercantil, etc.). Devuelve la " - "respuesta junto con las fuentes legales citadas (ley y " - "artículo) y un nivel de confianza. Input: una pregunta legal " - "en español, texto plano." + "Útil para obtener información legal general sobre leyes " + "federales mexicanas (laboral, civil, fiscal, penal, " + "mercantil, etc.). Devuelve la respuesta junto con las fuentes " + "legales en las que se basó (ley y artículo) y un nivel de " + "confianza. No sustituye asesoría legal profesional; verifica " + "la información con la fuente oficial o un abogado antes de " + "tomar decisiones. Input: una pregunta legal en español, texto " + "plano." ) args_schema: Type[BaseModel] = LexMexInput @@ -112,8 +133,15 @@ def _run(self, pregunta: str) -> str: "X-LexMex-Client": LEXMEX_CLIENT_ID, }, timeout=self.timeout, + allow_redirects=False, ) + if resp.is_redirect or resp.is_permanent_redirect or 300 <= resp.status_code < 400: + return ( + "Error: LEX-MEX respondió con una redirección inesperada; " + "la consulta se abortó por seguridad (la API key no se reenvía " + "a hosts distintos de lex-mex.xyz)." + ) if resp.status_code == 401: return "Error: API key de LEX-MEX inválida o revocada." if resp.status_code == 402: @@ -121,16 +149,29 @@ def _run(self, pregunta: str) -> str: if resp.status_code == 429: return "Error: límite diario de consultas de LEX-MEX alcanzado." resp.raise_for_status() - data = resp.json() - fuentes = data.get("fuentes") or [] + try: + data = resp.json() + except ValueError: + return "Error: LEX-MEX devolvió una respuesta no válida (JSON malformado)." + + if not isinstance(data, dict): + return "Error: LEX-MEX devolvió una respuesta con formato inesperado." + + respuesta = data.get("respuesta") + if not isinstance(respuesta, str) or not respuesta.strip(): + return "Error: LEX-MEX no devolvió una respuesta legal válida." + + fuentes = data.get("fuentes") + if not isinstance(fuentes, list): + fuentes = [] fuentes_txt = "; ".join( f.get("cita", str(f)) if isinstance(f, dict) else str(f) for f in fuentes ) or "sin fuentes citadas" return ( - f"{data.get('respuesta', '')}\n\n" + f"{respuesta}\n\n" f"Fuentes: {fuentes_txt}\n" f"Confianza: {data.get('confianza', 'n/d')}" ) From cd35a6c7ee36d33a44fec88c00b4b210915be991 Mon Sep 17 00:00:00 2001 From: Volpsmx Date: Thu, 20 Aug 2026 14:34:14 -0600 Subject: [PATCH 10/12] Fix null cita crash, simplify redirect check, sync test mocks Simplified redirect validation to rely solely on status_code and added protection against non-string citation values in fuentes_txt. --- .../src/crewai_tools/tools/lexmex_tool/lexmex_tool.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/crewai-tools/src/crewai_tools/tools/lexmex_tool/lexmex_tool.py b/lib/crewai-tools/src/crewai_tools/tools/lexmex_tool/lexmex_tool.py index d8e49db4dd..5362899cba 100644 --- a/lib/crewai-tools/src/crewai_tools/tools/lexmex_tool/lexmex_tool.py +++ b/lib/crewai-tools/src/crewai_tools/tools/lexmex_tool/lexmex_tool.py @@ -48,6 +48,10 @@ docstring/description ("no alucina", "cita exacta") por lenguaje que describe la salida como información legal general y recomienda verificarla con la fuente oficial o un abogado. +- Se simplificó la validación de redirects para depender únicamente de + `status_code` (evita requerir `is_redirect`/`is_permanent_redirect`, + que no todo doble de prueba expone) y se blindó `fuentes_txt` contra + valores `cita` no-string (p. ej. `null`), que antes rompían el join. """ from __future__ import annotations @@ -136,7 +140,7 @@ def _run(self, pregunta: str) -> str: allow_redirects=False, ) - if resp.is_redirect or resp.is_permanent_redirect or 300 <= resp.status_code < 400: + if 300 <= resp.status_code < 400: return ( "Error: LEX-MEX respondió con una redirección inesperada; " "la consulta se abortó por seguridad (la API key no se reenvía " @@ -166,7 +170,9 @@ def _run(self, pregunta: str) -> str: if not isinstance(fuentes, list): fuentes = [] fuentes_txt = "; ".join( - f.get("cita", str(f)) if isinstance(f, dict) else str(f) + f.get("cita") + if isinstance(f, dict) and isinstance(f.get("cita"), str) + else str(f) for f in fuentes ) or "sin fuentes citadas" From 28f07faf134663e5fb6471d7755bb0ff06efac1c Mon Sep 17 00:00:00 2001 From: Volpsmx Date: Thu, 20 Aug 2026 14:35:30 -0600 Subject: [PATCH 11/12] Fix null cita crash, simplify redirect check, sync test mocks Updated fake_post function to include allow_redirects parameter for better control over HTTP requests. --- .../src/lib/crewai-tools/tests/tools/test_lexmex_tool.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/crewai-tools/src/lib/crewai-tools/tests/tools/test_lexmex_tool.py b/lib/crewai-tools/src/lib/crewai-tools/tests/tools/test_lexmex_tool.py index bfde9ca5ef..79ab5a4b19 100644 --- a/lib/crewai-tools/src/lib/crewai-tools/tests/tools/test_lexmex_tool.py +++ b/lib/crewai-tools/src/lib/crewai-tools/tests/tools/test_lexmex_tool.py @@ -50,11 +50,12 @@ def json(self): "confianza": "alta", } - def fake_post(url, json, headers, timeout): + def fake_post(url, json, headers, timeout, allow_redirects): captured["url"] = url captured["json"] = json captured["headers"] = headers captured["timeout"] = timeout + captured["allow_redirects"] = allow_redirects return FakeResponse() monkeypatch.setattr( @@ -69,6 +70,7 @@ def fake_post(url, json, headers, timeout): assert captured["headers"]["X-API-Key"] == "lmx_live_test" assert captured["headers"]["X-LexMex-Client"] == "crewai-lexmex-tool/1.1.0" assert captured["timeout"] == 30 + assert captured["allow_redirects"] is False assert "despido justificado" in resultado assert "LFT, art. 47" in resultado From a05d9f3ed4e114e5293c57ce6e793906efa124c8 Mon Sep 17 00:00:00 2001 From: Volpsmx Date: Thu, 20 Aug 2026 15:02:54 -0600 Subject: [PATCH 12/12] Add regression test for 3xx redirect rejection Add test to ensure redirects are handled correctly. --- .../tests/tools/test_lexmex_tool.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/lib/crewai-tools/src/lib/crewai-tools/tests/tools/test_lexmex_tool.py b/lib/crewai-tools/src/lib/crewai-tools/tests/tools/test_lexmex_tool.py index 79ab5a4b19..63867c6189 100644 --- a/lib/crewai-tools/src/lib/crewai-tools/tests/tools/test_lexmex_tool.py +++ b/lib/crewai-tools/src/lib/crewai-tools/tests/tools/test_lexmex_tool.py @@ -120,3 +120,26 @@ class FakeResponse: resultado = tool.run(pregunta="cualquier pregunta") assert "límite" in resultado.lower() + + +def test_redirect_is_rejected_without_reading_body(monkeypatch): + """CodeRabbit: ante un 3xx, la tool debe cortar antes de llamar + .json() (defensa contra que alguien quite ese return a futuro y + la key termine yendo a un host fuera de lex-mex.xyz).""" + tool = LexMexTool(api_key="lmx_live_test") + + class FakeRedirectResponse: + status_code = 302 + + def json(self): + raise AssertionError( + ".json() no debe llamarse ante una respuesta de redirect" + ) + + monkeypatch.setattr( + "crewai_tools.tools.lexmex_tool.lexmex_tool.requests.post", + lambda *a, **kw: FakeRedirectResponse(), + ) + + resultado = tool.run(pregunta="cualquier pregunta") + assert "redirección" in resultado.lower()