Compare commits

..

3 Commits

Author SHA1 Message Date
Lorenze Jay
20373f673d chore: hotfix update for crewAI version to 0.203.2 2025-11-22 09:43:17 -08:00
Lorenze Jay
814c962196 chore: update crewAI version to 0.203.1 in multiple templates (#3699)
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Notify Downstream / notify-downstream (push) Has been cancelled
Update Test Durations / update-durations (3.10) (push) Has been cancelled
Update Test Durations / update-durations (3.11) (push) Has been cancelled
Update Test Durations / update-durations (3.12) (push) Has been cancelled
Update Test Durations / update-durations (3.13) (push) Has been cancelled
- Bumped the `crewai` version in `__init__.py` to 0.203.1.
- Updated the dependency versions in the crew, flow, and tool templates' `pyproject.toml` files to reflect the new `crewai` version.
2025-10-13 11:46:22 -07:00
Heitor Carvalho
2ebb2e845f fix: add a leeway of 10s when decoding jwt (#3698) 2025-10-13 12:42:03 -03:00
8 changed files with 28 additions and 202 deletions

View File

@@ -775,4 +775,3 @@ A: Yes, CrewAI provides extensive beginner-friendly tutorials, courses, and docu
### Q: Can CrewAI automate human-in-the-loop workflows?
A: Yes, CrewAI fully supports human-in-the-loop workflows, allowing seamless collaboration between human experts and AI agents for enhanced decision-making.
# test

View File

@@ -40,7 +40,7 @@ def _suppress_pydantic_deprecation_warnings() -> None:
_suppress_pydantic_deprecation_warnings()
__version__ = "0.203.0"
__version__ = "0.203.2"
_telemetry_submitted = False

View File

@@ -30,6 +30,7 @@ def validate_jwt_token(
algorithms=["RS256"],
audience=audience,
issuer=issuer,
leeway=10.0,
options={
"verify_signature": True,
"verify_exp": True,

View File

@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
authors = [{ name = "Your Name", email = "you@example.com" }]
requires-python = ">=3.10,<3.14"
dependencies = [
"crewai[tools]>=0.203.0,<1.0.0"
"crewai[tools]>=0.203.1,<1.0.0"
]
[project.scripts]

View File

@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
authors = [{ name = "Your Name", email = "you@example.com" }]
requires-python = ">=3.10,<3.14"
dependencies = [
"crewai[tools]>=0.203.0,<1.0.0",
"crewai[tools]>=0.203.1,<1.0.0",
]
[project.scripts]

View File

@@ -5,7 +5,7 @@ description = "Power up your crews with {{folder_name}}"
readme = "README.md"
requires-python = ">=3.10,<3.14"
dependencies = [
"crewai[tools]>=0.203.0"
"crewai[tools]>=0.203.1"
]
[tool.crewai]

View File

@@ -1,6 +1,6 @@
import ast
import datetime
import json
import re
import time
from difflib import SequenceMatcher
from json import JSONDecodeError
@@ -44,183 +44,6 @@ OPENAI_BIGGER_MODELS = [
]
def _safe_literal_parse(input_str: str) -> Any:
"""
Safely parse a limited subset of Python literal syntax without using ast.literal_eval.
Only supports: strings (single/double quotes), numbers, booleans, None, lists, dicts.
Rejects any input that could lead to code execution.
Args:
input_str: String to parse
Returns:
Parsed Python object
Raises:
ValueError: If input contains unsafe or unsupported syntax
"""
if not isinstance(input_str, str):
raise ValueError("Input must be a string")
stripped = input_str.strip()
if not stripped:
raise ValueError("Input cannot be empty")
# Check for potentially dangerous patterns
dangerous_patterns = [
r'__.*__', # dunder methods
r'import\b', # import statements
r'exec\b', # exec function
r'eval\b', # eval function
r'lambda\b', # lambda functions
r'def\b', # function definitions
r'class\b', # class definitions
r'@\w+', # decorators
r'\.\.\.', # ellipsis (could be used in slicing)
r'->[^\]]*\]', # type hints in lists
]
for pattern in dangerous_patterns:
if re.search(pattern, stripped, re.IGNORECASE):
raise ValueError(f"Potentially dangerous pattern detected: {pattern}")
# Only allow specific characters
allowed_chars = r'[\s\w\.\-\+\*/\(\)\[\]\{\}:\'"<>!=,!=\?%&|~^`]'
if not re.fullmatch(f'{allowed_chars}*', stripped):
raise ValueError("Input contains unsupported characters")
# Try JSON parsing first (safest)
try:
return json.loads(stripped)
except (json.JSONDecodeError, TypeError):
pass
# Manual parsing for simple Python literals (JSON with single quotes, etc.)
try:
return _parse_python_literal_safe(stripped)
except Exception as e:
raise ValueError(f"Failed to parse input safely: {e}")
def _parse_python_literal_safe(input_str: str) -> Any:
"""
Parse a limited subset of Python literals safely.
Args:
input_str: String to parse
Returns:
Parsed Python object
"""
# Handle None
if input_str == 'None':
return None
# Handle booleans
if input_str == 'True':
return True
if input_str == 'False':
return False
# Handle numbers
if re.fullmatch(r'-?\d+$', input_str):
return int(input_str)
if re.fullmatch(r'-?\d+\.\d+$', input_str):
return float(input_str)
# Handle strings with single quotes (convert to JSON format)
if (input_str.startswith("'") and input_str.endswith("'")) or \
(input_str.startswith('"') and input_str.endswith('"')):
# Simple string - just remove quotes and escape common sequences
inner = input_str[1:-1]
# Handle common escape sequences safely
inner = inner.replace("\\'", "'").replace('\\"', '"').replace("\\\\", "\\")
return inner
# Handle lists
if input_str.startswith('[') and input_str.endswith(']'):
inner = input_str[1:-1].strip()
if not inner:
return []
items = _split_items_safe(inner)
return [_parse_python_literal_safe(item.strip()) for item in items]
# Handle dictionaries
if input_str.startswith('{') and input_str.endswith('}'):
inner = input_str[1:-1].strip()
if not inner:
return {}
pairs = _split_items_safe(inner)
result = {}
for pair in pairs:
if ':' not in pair:
raise ValueError(f"Invalid dict pair: {pair}")
key_str, value_str = pair.split(':', 1)
key = _parse_python_literal_safe(key_str.strip())
value = _parse_python_literal_safe(value_str.strip())
if not isinstance(key, str):
raise ValueError(f"Dict keys must be strings, got {type(key)}")
result[key] = value
return result
raise ValueError(f"Unsupported literal format: {input_str}")
def _split_items_safe(input_str: str, delimiter: str = ',') -> list[str]:
"""
Split a list or dict string into items, respecting nested structures.
Args:
input_str: String to split
delimiter: Delimiter to split on
Returns:
List of item strings
"""
items = []
current = []
depth = 0
in_string = False
string_char = None
i = 0
while i < len(input_str):
char = input_str[i]
# Handle string literals
if char in ('"', "'") and (i == 0 or input_str[i-1] != '\\'):
if not in_string:
in_string = True
string_char = char
elif char == string_char:
in_string = False
string_char = None
# Track nesting depth when not in strings
elif not in_string:
if char in ('[', '(', '{'):
depth += 1
elif char in (']', ')', '}'):
depth -= 1
elif char == delimiter and depth == 0:
items.append(''.join(current).strip())
current = []
i += 1
continue
current.append(char)
i += 1
if current:
items.append(''.join(current).strip())
return items
class ToolUsageError(Exception):
"""Exception raised for errors in the tool usage."""
@@ -701,14 +524,14 @@ class ToolUsage:
except (JSONDecodeError, TypeError):
pass # Continue to the next parsing attempt
# Attempt 2: Parse as Python literal (safe alternative to ast.literal_eval)
# Attempt 2: Parse as Python literal
try:
arguments = _safe_literal_parse(tool_input)
arguments = ast.literal_eval(tool_input)
if isinstance(arguments, dict):
return arguments
except ValueError:
except (ValueError, SyntaxError):
repaired_input = repair_json(tool_input)
# Continue to the next parsing attempt
pass
# Attempt 3: Parse as JSON5
try:

View File

@@ -1,7 +1,7 @@
import jwt
import unittest
from unittest.mock import MagicMock, patch
import jwt
from crewai.cli.authentication.utils import validate_jwt_token
@@ -17,19 +17,22 @@ class TestUtils(unittest.TestCase):
key="mock_signing_key"
)
jwt_token = "aaaaa.bbbbbb.cccccc" # noqa: S105
decoded_token = validate_jwt_token(
jwt_token="aaaaa.bbbbbb.cccccc",
jwt_token=jwt_token,
jwks_url="https://mock_jwks_url",
issuer="https://mock_issuer",
audience="app_id_xxxx",
)
mock_jwt.decode.assert_called_with(
"aaaaa.bbbbbb.cccccc",
jwt_token,
"mock_signing_key",
algorithms=["RS256"],
audience="app_id_xxxx",
issuer="https://mock_issuer",
leeway=10.0,
options={
"verify_signature": True,
"verify_exp": True,
@@ -43,9 +46,9 @@ class TestUtils(unittest.TestCase):
def test_validate_jwt_token_expired(self, mock_jwt, mock_pyjwkclient):
mock_jwt.decode.side_effect = jwt.ExpiredSignatureError
with self.assertRaises(Exception):
with self.assertRaises(Exception): # noqa: B017
validate_jwt_token(
jwt_token="aaaaa.bbbbbb.cccccc",
jwt_token="aaaaa.bbbbbb.cccccc", # noqa: S106
jwks_url="https://mock_jwks_url",
issuer="https://mock_issuer",
audience="app_id_xxxx",
@@ -53,9 +56,9 @@ class TestUtils(unittest.TestCase):
def test_validate_jwt_token_invalid_audience(self, mock_jwt, mock_pyjwkclient):
mock_jwt.decode.side_effect = jwt.InvalidAudienceError
with self.assertRaises(Exception):
with self.assertRaises(Exception): # noqa: B017
validate_jwt_token(
jwt_token="aaaaa.bbbbbb.cccccc",
jwt_token="aaaaa.bbbbbb.cccccc", # noqa: S106
jwks_url="https://mock_jwks_url",
issuer="https://mock_issuer",
audience="app_id_xxxx",
@@ -63,9 +66,9 @@ class TestUtils(unittest.TestCase):
def test_validate_jwt_token_invalid_issuer(self, mock_jwt, mock_pyjwkclient):
mock_jwt.decode.side_effect = jwt.InvalidIssuerError
with self.assertRaises(Exception):
with self.assertRaises(Exception): # noqa: B017
validate_jwt_token(
jwt_token="aaaaa.bbbbbb.cccccc",
jwt_token="aaaaa.bbbbbb.cccccc", # noqa: S106
jwks_url="https://mock_jwks_url",
issuer="https://mock_issuer",
audience="app_id_xxxx",
@@ -75,9 +78,9 @@ class TestUtils(unittest.TestCase):
self, mock_jwt, mock_pyjwkclient
):
mock_jwt.decode.side_effect = jwt.MissingRequiredClaimError
with self.assertRaises(Exception):
with self.assertRaises(Exception): # noqa: B017
validate_jwt_token(
jwt_token="aaaaa.bbbbbb.cccccc",
jwt_token="aaaaa.bbbbbb.cccccc", # noqa: S106
jwks_url="https://mock_jwks_url",
issuer="https://mock_issuer",
audience="app_id_xxxx",
@@ -85,9 +88,9 @@ class TestUtils(unittest.TestCase):
def test_validate_jwt_token_jwks_error(self, mock_jwt, mock_pyjwkclient):
mock_jwt.decode.side_effect = jwt.exceptions.PyJWKClientError
with self.assertRaises(Exception):
with self.assertRaises(Exception): # noqa: B017
validate_jwt_token(
jwt_token="aaaaa.bbbbbb.cccccc",
jwt_token="aaaaa.bbbbbb.cccccc", # noqa: S106
jwks_url="https://mock_jwks_url",
issuer="https://mock_issuer",
audience="app_id_xxxx",
@@ -95,9 +98,9 @@ class TestUtils(unittest.TestCase):
def test_validate_jwt_token_invalid_token(self, mock_jwt, mock_pyjwkclient):
mock_jwt.decode.side_effect = jwt.InvalidTokenError
with self.assertRaises(Exception):
with self.assertRaises(Exception): # noqa: B017
validate_jwt_token(
jwt_token="aaaaa.bbbbbb.cccccc",
jwt_token="aaaaa.bbbbbb.cccccc", # noqa: S106
jwks_url="https://mock_jwks_url",
issuer="https://mock_issuer",
audience="app_id_xxxx",